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
130
go/couch/couch.go
Normal file
130
go/couch/couch.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package couch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-kivik/kivik/v4"
|
||||
_ "github.com/go-kivik/kivik/v4/couchdb" // The CouchDB driver
|
||||
"mail2couch/config"
|
||||
"mail2couch/mail"
|
||||
)
|
||||
|
||||
// Client wraps the Kivik client
|
||||
type Client struct {
|
||||
*kivik.Client
|
||||
}
|
||||
|
||||
// MailDocument represents an email message stored in CouchDB
|
||||
type MailDocument struct {
|
||||
ID string `json:"_id,omitempty"`
|
||||
Rev string `json:"_rev,omitempty"`
|
||||
SourceUID string `json:"sourceUid"` // Unique ID from the mail source (e.g., IMAP UID)
|
||||
Mailbox string `json:"mailbox"` // Source mailbox name
|
||||
From []string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Date time.Time `json:"date"`
|
||||
Body string `json:"body"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
StoredAt time.Time `json:"storedAt"` // When the document was stored
|
||||
DocType string `json:"docType"` // Always "mail"
|
||||
}
|
||||
|
||||
// NewClient creates a new CouchDB client from the configuration
|
||||
func NewClient(cfg *config.CouchDbConfig) (*Client, error) {
|
||||
parsedURL, err := url.Parse(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid couchdb url: %w", err)
|
||||
}
|
||||
|
||||
parsedURL.User = url.UserPassword(cfg.User, cfg.Password)
|
||||
dsn := parsedURL.String()
|
||||
|
||||
client, err := kivik.New("couch", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{client}, nil
|
||||
}
|
||||
|
||||
// EnsureDB ensures that the configured database exists.
|
||||
func (c *Client) EnsureDB(ctx context.Context, dbName string) error {
|
||||
exists, err := c.DBExists(ctx, dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return c.CreateDB(ctx, dbName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertMessage converts an IMAP message to a MailDocument
|
||||
func ConvertMessage(msg *mail.Message, mailbox string) *MailDocument {
|
||||
docID := fmt.Sprintf("%s_%d", mailbox, msg.UID)
|
||||
|
||||
return &MailDocument{
|
||||
ID: docID,
|
||||
SourceUID: fmt.Sprintf("%d", msg.UID),
|
||||
Mailbox: mailbox,
|
||||
From: msg.From,
|
||||
To: msg.To,
|
||||
Subject: msg.Subject,
|
||||
Date: msg.Date,
|
||||
Body: msg.Body,
|
||||
Headers: msg.Headers,
|
||||
StoredAt: time.Now(),
|
||||
DocType: "mail",
|
||||
}
|
||||
}
|
||||
|
||||
// StoreMessage stores a mail message in CouchDB
|
||||
func (c *Client) StoreMessage(ctx context.Context, dbName string, doc *MailDocument) error {
|
||||
db := c.DB(dbName)
|
||||
if db.Err() != nil {
|
||||
return db.Err()
|
||||
}
|
||||
|
||||
// Check if document already exists
|
||||
exists, err := c.DocumentExists(ctx, dbName, doc.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if document exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return nil // Document already exists, skip
|
||||
}
|
||||
|
||||
// Store the document
|
||||
_, err = db.Put(ctx, doc.ID, doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store document: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreMessages stores multiple mail messages in CouchDB
|
||||
func (c *Client) StoreMessages(ctx context.Context, dbName string, docs []*MailDocument) error {
|
||||
for _, doc := range docs {
|
||||
if err := c.StoreMessage(ctx, dbName, doc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DocumentExists checks if a document with the given ID already exists.
|
||||
func (c *Client) DocumentExists(ctx context.Context, dbName, docID string) (bool, error) {
|
||||
db := c.DB(dbName)
|
||||
if db.Err() != nil {
|
||||
return false, db.Err()
|
||||
}
|
||||
|
||||
row := db.Get(ctx, docID)
|
||||
return row.Err() == nil, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue