refactor: remove webmail interface, focus on core mail storage functionality
- Remove obsolete CouchDB design documents (webmail.json, dashboard.json) - Clean up webmail-related code from couch/couch.go (WebmailViews, CreateWebmailViews, etc.) - Update documentation to focus on core mail-to-CouchDB storage functionality - Add Future Plans section describing planned webmail viewer as separate component - Apply go fmt formatting and ensure code quality standards - Update test documentation to show raw CouchDB API access patterns - Remove compiled binary from repository This refactor simplifies the codebase to focus on its core purpose: efficiently backing up emails from IMAP to CouchDB. The webmail interface will be developed as a separate, optional component to maintain clean separation of concerns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c2ad55eaaf
commit
e280aa0aaa
12 changed files with 147 additions and 49 deletions
|
|
@ -40,7 +40,7 @@ type FolderFilter struct {
|
|||
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
|
||||
SenderKeywords []string `json:"senderKeywords,omitempty"` // Filter by keywords in sender addresses
|
||||
RecipientKeywords []string `json:"recipientKeywords,omitempty"` // Filter by keywords in recipient addresses
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,20 +22,20 @@ type Client struct {
|
|||
|
||||
// MailDocument represents an email message stored in CouchDB
|
||||
type MailDocument struct {
|
||||
ID string `json:"_id,omitempty"`
|
||||
Rev string `json:"_rev,omitempty"`
|
||||
Attachments map[string]AttachmentStub `json:"_attachments,omitempty"` // CouchDB attachments
|
||||
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"
|
||||
HasAttachments bool `json:"hasAttachments"` // Indicates if message has attachments
|
||||
ID string `json:"_id,omitempty"`
|
||||
Rev string `json:"_rev,omitempty"`
|
||||
Attachments map[string]AttachmentStub `json:"_attachments,omitempty"` // CouchDB attachments
|
||||
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"
|
||||
HasAttachments bool `json:"hasAttachments"` // Indicates if message has attachments
|
||||
}
|
||||
|
||||
// AttachmentStub represents metadata for a CouchDB attachment
|
||||
|
|
@ -94,19 +94,19 @@ func GenerateAccountDBName(accountName, userEmail string) string {
|
|||
if name == "" {
|
||||
name = userEmail
|
||||
}
|
||||
|
||||
|
||||
// Convert to lowercase and replace invalid characters with underscores
|
||||
name = strings.ToLower(name)
|
||||
// CouchDB database names must match: ^[a-z][a-z0-9_$()+/-]*$
|
||||
validName := regexp.MustCompile(`[^a-z0-9_$()+/-]`).ReplaceAllString(name, "_")
|
||||
|
||||
|
||||
// Ensure it starts with a letter and add m2c prefix
|
||||
if len(validName) > 0 && (validName[0] < 'a' || validName[0] > 'z') {
|
||||
validName = "m2c_mail_" + validName
|
||||
} else {
|
||||
validName = "m2c_" + validName
|
||||
}
|
||||
|
||||
|
||||
return validName
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +228,7 @@ func (c *Client) GetAllMailDocumentIDs(ctx context.Context, dbName, mailbox stri
|
|||
|
||||
// Create a view query to get all document IDs for the specified mailbox
|
||||
rows := db.AllDocs(ctx)
|
||||
|
||||
|
||||
docIDs := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
docID, err := rows.ID()
|
||||
|
|
@ -240,11 +240,11 @@ func (c *Client) GetAllMailDocumentIDs(ctx context.Context, dbName, mailbox stri
|
|||
docIDs[docID] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if rows.Err() != nil {
|
||||
return nil, rows.Err()
|
||||
}
|
||||
|
||||
|
||||
return docIDs, nil
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ func (c *Client) SyncMailbox(ctx context.Context, dbName, mailbox string, curren
|
|||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
uidStr := parts[len(parts)-1]
|
||||
uid := uint32(0)
|
||||
if _, err := fmt.Sscanf(uidStr, "%d", &uid); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ go 1.24.4
|
|||
|
||||
require (
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||
github.com/emersion/go-message v0.18.1
|
||||
github.com/go-kivik/kivik/v4 v4.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/emersion/go-message v0.18.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
// First, get all current UIDs in the mailbox for sync purposes
|
||||
allUIDsSet := imap.SeqSet{}
|
||||
allUIDsSet.AddRange(1, mbox.NumMessages)
|
||||
|
||||
|
||||
// Fetch UIDs for all messages to track current state
|
||||
uidCmd := c.Fetch(allUIDsSet, &imap.FetchOptions{UID: true})
|
||||
for {
|
||||
|
|
@ -112,12 +112,12 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
if msg == nil {
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
data, err := msg.Collect()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if data.UID != 0 {
|
||||
currentUIDs[uint32(data.UID)] = true
|
||||
}
|
||||
|
|
@ -126,13 +126,13 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
|
||||
// Determine which messages to fetch based on since date
|
||||
var seqSet imap.SeqSet
|
||||
|
||||
|
||||
if since != nil {
|
||||
// Use IMAP SEARCH to find messages since the specified date
|
||||
searchCriteria := &imap.SearchCriteria{
|
||||
Since: *since,
|
||||
}
|
||||
|
||||
|
||||
searchCmd := c.Search(searchCriteria, nil)
|
||||
searchResults, err := searchCmd.Wait()
|
||||
if err != nil {
|
||||
|
|
@ -149,12 +149,12 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
if len(searchSeqNums) == 0 {
|
||||
return []*Message{}, currentUIDs, nil
|
||||
}
|
||||
|
||||
|
||||
// Limit results if maxMessages is specified
|
||||
if maxMessages > 0 && len(searchSeqNums) > maxMessages {
|
||||
searchSeqNums = searchSeqNums[len(searchSeqNums)-maxMessages:]
|
||||
}
|
||||
|
||||
|
||||
for _, seqNum := range searchSeqNums {
|
||||
seqSet.AddNum(seqNum)
|
||||
}
|
||||
|
|
@ -165,11 +165,11 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
if maxMessages > 0 && int(numToFetch) > maxMessages {
|
||||
numToFetch = uint32(maxMessages)
|
||||
}
|
||||
|
||||
|
||||
if numToFetch == 0 {
|
||||
return []*Message{}, currentUIDs, nil
|
||||
}
|
||||
|
||||
|
||||
// Fetch the most recent messages
|
||||
seqSet.AddRange(mbox.NumMessages-numToFetch+1, mbox.NumMessages)
|
||||
}
|
||||
|
|
@ -177,12 +177,12 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
// Fetch message data - get envelope and full message body
|
||||
options := &imap.FetchOptions{
|
||||
Envelope: true,
|
||||
UID: true,
|
||||
UID: true,
|
||||
BodySection: []*imap.FetchItemBodySection{
|
||||
{}, // Empty section gets the entire message
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
fetchCmd := c.Fetch(seqSet, options)
|
||||
|
||||
for {
|
||||
|
|
@ -196,12 +196,12 @@ func (c *ImapClient) GetMessages(mailbox string, since *time.Time, maxMessages i
|
|||
log.Printf("Failed to parse message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Apply message-level keyword filtering
|
||||
if messageFilter != nil && !c.ShouldProcessMessage(parsedMsg, messageFilter) {
|
||||
continue // Skip this message due to keyword filter
|
||||
}
|
||||
|
||||
|
||||
messages = append(messages, parsedMsg)
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ func (c *ImapClient) parseMessage(fetchMsg *imapclient.FetchMessageData) (*Messa
|
|||
env := buffer.Envelope
|
||||
msg.Subject = env.Subject
|
||||
msg.Date = env.Date
|
||||
|
||||
|
||||
// Parse From addresses
|
||||
for _, addr := range env.From {
|
||||
if addr.Mailbox != "" {
|
||||
|
|
@ -242,7 +242,7 @@ func (c *ImapClient) parseMessage(fetchMsg *imapclient.FetchMessageData) (*Messa
|
|||
msg.From = append(msg.From, fullAddr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Parse To addresses
|
||||
for _, addr := range env.To {
|
||||
if addr.Mailbox != "" {
|
||||
|
|
@ -264,7 +264,7 @@ func (c *ImapClient) parseMessage(fetchMsg *imapclient.FetchMessageData) (*Messa
|
|||
if len(buffer.BodySection) > 0 {
|
||||
bodyBuffer := buffer.BodySection[0]
|
||||
reader := bytes.NewReader(bodyBuffer.Bytes)
|
||||
|
||||
|
||||
// Parse the message using go-message
|
||||
entity, err := message.Read(reader)
|
||||
if err != nil {
|
||||
|
|
@ -338,7 +338,7 @@ func (c *ImapClient) parseMessagePart(entity *message.Entity, msg *Message) erro
|
|||
disposition, dispositionParams, _ := entity.Header.ContentDisposition()
|
||||
|
||||
// Determine if this is an attachment
|
||||
isAttachment := disposition == "attachment" ||
|
||||
isAttachment := disposition == "attachment" ||
|
||||
(disposition == "inline" && dispositionParams["filename"] != "") ||
|
||||
params["name"] != ""
|
||||
|
||||
|
|
|
|||
BIN
go/mail2couch
BIN
go/mail2couch
Binary file not shown.
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
func main() {
|
||||
args := config.ParseCommandLine()
|
||||
|
||||
|
||||
cfg, err := config.LoadConfigWithDiscovery(args)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
|
|
@ -33,12 +33,12 @@ func main() {
|
|||
|
||||
// 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
|
||||
|
|
@ -111,7 +111,7 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
|||
if syncMetadata != nil {
|
||||
// Use last sync time for incremental sync
|
||||
sinceDate = &syncMetadata.LastSyncTime
|
||||
fmt.Printf(" Incremental sync since: %s (last synced %d messages)\n",
|
||||
fmt.Printf(" Incremental sync since: %s (last synced %d messages)\n",
|
||||
sinceDate.Format("2006-01-02 15:04:05"), syncMetadata.MessageCount)
|
||||
} else {
|
||||
// First sync - use config since date if available
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue