feat: implement real IMAP message parsing with native CouchDB attachments
- Replace placeholder message generation with actual IMAP message fetching using go-message library - Add per-account CouchDB databases for better organization and isolation - Implement native CouchDB attachment storage with proper revision management - Add command line argument parsing with --max-messages flag for controlling message processing limits - Support both sync and archive modes with proper document synchronization - Add comprehensive test environment with Podman containers (GreenMail IMAP server + CouchDB) - Implement full MIME multipart parsing for proper body and attachment extraction - Add TLS and plain IMAP connection support based on port configuration - Update configuration system to support sync vs archive modes - Create test scripts and sample data for development and testing Key technical improvements: - Real email envelope and header processing with go-imap v2 API - MIME Content-Type and Content-Disposition parsing for attachment detection - CouchDB document ID generation using mailbox_uid format for uniqueness - Duplicate detection and prevention to avoid re-storing existing messages - Proper error handling and connection management for IMAP operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
79f19a8877
commit
ea6235b674
22 changed files with 1262 additions and 66 deletions
|
|
@ -28,7 +28,7 @@ type MailSource struct {
|
|||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Sync bool `json:"sync"`
|
||||
Mode string `json:"mode"` // "sync" or "archive"
|
||||
FolderFilter FolderFilter `json:"folderFilter"`
|
||||
MessageFilter MessageFilter `json:"messageFilter"`
|
||||
}
|
||||
|
|
@ -59,24 +59,59 @@ func LoadConfig(path string) (*Config, error) {
|
|||
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() (string, error) {
|
||||
// Check for command line flag
|
||||
configFlag := flag.String("config", "", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
if *configFlag != "" {
|
||||
if _, err := os.Stat(*configFlag); err == nil {
|
||||
return *configFlag, nil
|
||||
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", *configFlag)
|
||||
return "", fmt.Errorf("specified config file not found: %s", args.ConfigPath)
|
||||
}
|
||||
|
||||
// List of possible config file locations in order of preference
|
||||
|
|
@ -104,12 +139,17 @@ func FindConfigFile() (string, error) {
|
|||
}
|
||||
|
||||
// LoadConfigWithDiscovery loads configuration using automatic file discovery
|
||||
func LoadConfigWithDiscovery() (*Config, error) {
|
||||
configPath, err := FindConfigFile()
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue