mail2couch/go/main.go
Ole-Morten Duesund ea6235b674 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>
2025-08-01 17:04:10 +02:00

150 lines
4.2 KiB
Go

package main
import (
"context"
"fmt"
"log"
"time"
"mail2couch/config"
"mail2couch/couch"
"mail2couch/mail"
)
func main() {
args := config.ParseCommandLine()
cfg, err := config.LoadConfigWithDiscovery(args)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Initialize CouchDB client
couchClient, err := couch.NewClient(&cfg.CouchDb)
if err != nil {
log.Fatalf("Failed to create CouchDB client: %v", err)
}
fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources))
for _, source := range cfg.MailSources {
if !source.Enabled {
continue
}
// Generate per-account database name
dbName := couch.GenerateAccountDBName(source.Name, source.User)
// Ensure the account-specific database exists
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err = couchClient.EnsureDB(ctx, dbName)
cancel()
if err != nil {
log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err)
continue
} else {
fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name)
}
fmt.Printf(" - Processing source: %s\n", source.Name)
if source.Protocol == "imap" {
err := processImapSource(&source, couchClient, dbName, args.MaxMessages)
if err != nil {
log.Printf(" ERROR: Failed to process IMAP source %s: %v", source.Name, err)
}
}
}
}
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int) error {
fmt.Printf(" Connecting to IMAP server: %s:%d\n", source.Host, source.Port)
imapClient, err := mail.NewImapClient(source)
if err != nil {
return fmt.Errorf("failed to connect to IMAP server: %w", err)
}
defer imapClient.Logout()
fmt.Println(" IMAP connection successful.")
mailboxes, err := imapClient.ListMailboxes()
if err != nil {
return fmt.Errorf("failed to list mailboxes: %w", err)
}
fmt.Printf(" Found %d mailboxes.\n", len(mailboxes))
// Parse the since date if provided
var sinceDate *time.Time
if source.MessageFilter.Since != "" {
parsed, err := time.Parse("2006-01-02", source.MessageFilter.Since)
if err != nil {
log.Printf(" WARNING: Invalid since date format '%s', ignoring filter", source.MessageFilter.Since)
} else {
sinceDate = &parsed
}
}
totalMessages := 0
totalStored := 0
// Process each mailbox
for _, mailbox := range mailboxes {
// Check if this mailbox should be processed based on filters
if !imapClient.ShouldProcessMailbox(mailbox, &source.FolderFilter) {
fmt.Printf(" Skipping mailbox: %s (filtered)\n", mailbox)
continue
}
fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode)
// Retrieve messages from the mailbox
messages, currentUIDs, err := imapClient.GetMessages(mailbox, sinceDate, maxMessages)
if err != nil {
log.Printf(" ERROR: Failed to get messages from %s: %v", mailbox, err)
continue
}
// Perform sync/archive logic
syncCtx, syncCancel := context.WithTimeout(context.Background(), 30*time.Second)
err = couchClient.SyncMailbox(syncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
syncCancel()
if err != nil {
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
continue
}
if len(messages) == 0 {
fmt.Printf(" No new messages found in %s\n", mailbox)
continue
}
fmt.Printf(" Found %d messages in %s\n", len(messages), mailbox)
totalMessages += len(messages)
// Convert messages to CouchDB documents
var docs []*couch.MailDocument
for _, msg := range messages {
doc := couch.ConvertMessage(msg, mailbox)
docs = append(docs, doc)
}
// Store messages in CouchDB with attachments
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
stored := 0
for i, doc := range docs {
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
if err != nil {
log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err)
} else {
stored++
}
}
cancel()
fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox)
totalStored += stored
}
fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
return nil
}