feat: implement Go-based mail2couch with working IMAP and CouchDB integration
- Add configuration system with automatic file discovery (current dir, config subdir, user home, XDG config) - Implement IMAP client with TLS connection, authentication, and mailbox listing - Add CouchDB integration with database creation and document storage - Support folder filtering (include/exclude) and date filtering (since parameter) - Include duplicate detection to prevent re-storing existing messages - Add comprehensive error handling and logging throughout - Structure code in clean packages: config, mail, couch - Application currently uses placeholder messages to test the storage pipeline - Ready for real IMAP message parsing implementation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d0caff800a
commit
1e4a67d4cb
9 changed files with 746 additions and 0 deletions
134
go/main.go
Normal file
134
go/main.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"mail2couch/config"
|
||||
"mail2couch/couch"
|
||||
"mail2couch/mail"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.LoadConfigWithDiscovery()
|
||||
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)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = couchClient.EnsureDB(ctx, cfg.CouchDb.Database)
|
||||
if err != nil {
|
||||
log.Printf("Could not ensure CouchDB database exists (is it running?): %v", err)
|
||||
} else {
|
||||
fmt.Printf("CouchDB database '%s' is ready.\n", cfg.CouchDb.Database)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources))
|
||||
for _, source := range cfg.MailSources {
|
||||
if !source.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" - Processing source: %s\n", source.Name)
|
||||
if source.Protocol == "imap" {
|
||||
err := processImapSource(&source, couchClient, cfg.CouchDb.Database)
|
||||
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) 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\n", mailbox)
|
||||
|
||||
// Retrieve messages from the mailbox
|
||||
messages, err := imapClient.GetMessages(mailbox, sinceDate)
|
||||
if err != nil {
|
||||
log.Printf(" ERROR: Failed to get messages from %s: %v", mailbox, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
fmt.Printf(" No 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
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
stored := 0
|
||||
for _, doc := range docs {
|
||||
err := couchClient.StoreMessage(ctx, dbName, doc)
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue