## Wildcard Folder Selection
- Add support for wildcard patterns (`*`, `?`, `[abc]`) using filepath.Match
- Implement special case: `"*"` selects ALL available folders
- Support for complex include/exclude pattern combinations
- Maintain backwards compatibility with exact string matching
- Enable subfolder pattern matching (e.g., `Work/*`, `*/Drafts`)
## Keyword Filtering
- Add SubjectKeywords, SenderKeywords, RecipientKeywords to MessageFilter config
- Implement case-insensitive keyword matching across message fields
- Support multiple keywords per filter type with inclusive OR logic
- Add ShouldProcessMessage method for message-level filtering
## Enhanced Test Environment
- Create comprehensive wildcard pattern test scenarios
- Add 12 test folders covering various pattern types: Work/*, Important/*, Archive/*, exact matches
- Implement dedicated wildcard test script (test-wildcard-patterns.sh)
- Update test configurations to demonstrate real-world wildcard usage patterns
- Enhance test data generation with folder-specific messages for validation
## Documentation
- Create FOLDER_PATTERNS.md with comprehensive wildcard examples and use cases
- Update CLAUDE.md to reflect all implemented features and current status
- Enhance test README with detailed wildcard pattern explanations
- Provide configuration examples for common email organization scenarios
## Message Origin Tracking
- Verify all messages in CouchDB properly tagged with origin folder in `mailbox` field
- Maintain per-account database isolation for better organization
- Document ID format: `{folder}_{uid}` ensures uniqueness across folders
Key patterns supported:
- `["*"]` - All folders (with excludes)
- `["Work*", "Important*"]` - Prefix matching
- `["Work/*", "Archive/*"]` - Subfolder patterns
- `["INBOX", "Sent"]` - Exact matches
- Complex include/exclude combinations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type Config struct {
|
|
CouchDb CouchDbConfig `json:"couchDb"`
|
|
MailSources []MailSource `json:"mailSources"`
|
|
}
|
|
|
|
type CouchDbConfig struct {
|
|
URL string `json:"url"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
Database string `json:"database"`
|
|
}
|
|
|
|
type MailSource struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
Protocol string `json:"protocol"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
Mode string `json:"mode"` // "sync" or "archive"
|
|
FolderFilter FolderFilter `json:"folderFilter"`
|
|
MessageFilter MessageFilter `json:"messageFilter"`
|
|
}
|
|
|
|
type FolderFilter struct {
|
|
Include []string `json:"include"`
|
|
Exclude []string `json:"exclude"`
|
|
}
|
|
|
|
type MessageFilter struct {
|
|
Since string `json:"since,omitempty"`
|
|
SubjectKeywords []string `json:"subjectKeywords,omitempty"` // Filter by keywords in subject
|
|
SenderKeywords []string `json:"senderKeywords,omitempty"` // Filter by keywords in sender addresses
|
|
RecipientKeywords []string `json:"recipientKeywords,omitempty"` // Filter by keywords in recipient addresses
|
|
}
|
|
|
|
func LoadConfig(path string) (*Config, error) {
|
|
configFile, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer configFile.Close()
|
|
|
|
var config Config
|
|
jsonParser := json.NewDecoder(configFile)
|
|
if err = jsonParser.Decode(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate and set defaults for mail sources
|
|
for i := range config.MailSources {
|
|
source := &config.MailSources[i]
|
|
if source.Mode == "" {
|
|
source.Mode = "archive" // Default to archive mode
|
|
}
|
|
if source.Mode != "sync" && source.Mode != "archive" {
|
|
return nil, fmt.Errorf("invalid mode '%s' for mail source '%s': must be 'sync' or 'archive'", source.Mode, source.Name)
|
|
}
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// IsSyncMode returns true if the mail source is in sync mode
|
|
func (ms *MailSource) IsSyncMode() bool {
|
|
return ms.Mode == "sync"
|
|
}
|
|
|
|
// IsArchiveMode returns true if the mail source is in archive mode
|
|
func (ms *MailSource) IsArchiveMode() bool {
|
|
return ms.Mode == "archive" || ms.Mode == "" // Default to archive
|
|
}
|
|
|
|
// CommandLineArgs holds parsed command line arguments
|
|
type CommandLineArgs struct {
|
|
ConfigPath string
|
|
MaxMessages int
|
|
}
|
|
|
|
// ParseCommandLine parses command line arguments
|
|
func ParseCommandLine() *CommandLineArgs {
|
|
configFlag := flag.String("config", "", "Path to configuration file")
|
|
maxMessagesFlag := flag.Int("max-messages", 0, "Maximum number of messages to process per mailbox per run (0 = no limit)")
|
|
flag.Parse()
|
|
|
|
return &CommandLineArgs{
|
|
ConfigPath: *configFlag,
|
|
MaxMessages: *maxMessagesFlag,
|
|
}
|
|
}
|
|
|
|
// FindConfigFile searches for config.json in the following order:
|
|
// 1. Path specified by -config flag
|
|
// 2. ./config.json (current directory)
|
|
// 3. ~/.config/mail2couch/config.json (user config directory)
|
|
// 4. ~/.mail2couch.json (user home directory)
|
|
func FindConfigFile(args *CommandLineArgs) (string, error) {
|
|
if args.ConfigPath != "" {
|
|
if _, err := os.Stat(args.ConfigPath); err == nil {
|
|
return args.ConfigPath, nil
|
|
}
|
|
return "", fmt.Errorf("specified config file not found: %s", args.ConfigPath)
|
|
}
|
|
|
|
// List of possible config file locations in order of preference
|
|
candidates := []string{
|
|
"config.json", // Current directory
|
|
"config/config.json", // Config subdirectory
|
|
}
|
|
|
|
// Add user directory paths
|
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
candidates = append(candidates,
|
|
filepath.Join(homeDir, ".config", "mail2couch", "config.json"),
|
|
filepath.Join(homeDir, ".mail2couch.json"),
|
|
)
|
|
}
|
|
|
|
// Try each candidate location
|
|
for _, candidate := range candidates {
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("no configuration file found. Searched locations: %v", candidates)
|
|
}
|
|
|
|
// LoadConfigWithDiscovery loads configuration using automatic file discovery
|
|
func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) {
|
|
configPath, err := FindConfigFile(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Printf("Using configuration file: %s\n", configPath)
|
|
if args.MaxMessages > 0 {
|
|
fmt.Printf("Maximum messages per mailbox: %d\n", args.MaxMessages)
|
|
} else {
|
|
fmt.Printf("Maximum messages per mailbox: unlimited\n")
|
|
}
|
|
return LoadConfig(configPath)
|
|
}
|