diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 4336df33ccb8e93d7d90904e6ae124edf3825058..aeb620ed10ffaadeb52ab3f70bf7fa1857f88608 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -1,6 +1,7 @@ package fetcher import ( + "bufio" "bytes" "crypto/tls" "crypto/x509" @@ -12,6 +13,7 @@ import ( "io/ioutil" "mime" "mime/quotedprintable" + "net/textproto" "os" "slices" "strings" @@ -80,6 +82,37 @@ func hasSeenFlag(flags []string) bool { return slices.Contains(flags, imap.SeenFlag) } +// deliveryHeadersMatch checks if any of the Delivered-To, X-Forwarded-To, or +// X-Original-To headers contain the given email address. This catches +// auto-forwarded emails where the envelope To/Cc don't match the local account. +func deliveryHeadersMatch(msg *imap.Message, section *imap.BodySectionName, fetchEmail string) bool { + if section == nil { + return false + } + literal := msg.GetBody(section) + if literal == nil { + return false + } + data, err := ioutil.ReadAll(literal) + if err != nil || len(data) == 0 { + return false + } + // Parse as MIME headers + reader := textproto.NewReader(bufio.NewReader(bytes.NewReader(data))) + headers, err := reader.ReadMIMEHeader() + if err != nil && len(headers) == 0 { + return false + } + for _, key := range []string{"Delivered-To", "X-Forwarded-To", "X-Original-To"} { + for _, val := range headers.Values(key) { + if strings.EqualFold(strings.TrimSpace(val), fetchEmail) { + return true + } + } + } + return false +} + func decodePart(reader io.Reader, header mail.PartHeader) (string, error) { mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type")) if err != nil { @@ -283,7 +316,10 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u messages := make(chan *imap.Message, chunkSize) done := make(chan error, 1) - fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchFlags} + // Fetch delivery headers to match auto-forwarded emails (Delivered-To, X-Forwarded-To, X-Original-To) + deliveryHeaderItem := imap.FetchItem("BODY.PEEK[HEADER.FIELDS (Delivered-To X-Forwarded-To X-Original-To)]") + deliveryHeaderSection, _ := imap.ParseBodySectionName(deliveryHeaderItem) + fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchFlags, deliveryHeaderItem} go func() { done <- c.Fetch(seqset, fetchItems, messages) @@ -334,6 +370,10 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u break } } + // Check delivery headers for auto-forwarded emails + if !matched { + matched = deliveryHeadersMatch(msg, deliveryHeaderSection, fetchEmail) + } } if !matched { @@ -1224,7 +1264,10 @@ func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email, messages := make(chan *imap.Message, limit) done := make(chan error, 1) - fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchFlags} + // Fetch delivery headers to match auto-forwarded emails + deliveryHeaderItem := imap.FetchItem("BODY.PEEK[HEADER.FIELDS (Delivered-To X-Forwarded-To X-Original-To)]") + deliveryHeaderSection, _ := imap.ParseBodySectionName(deliveryHeaderItem) + fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchFlags, deliveryHeaderItem} go func() { done <- c.Fetch(seqset, fetchItems, messages) }() @@ -1278,6 +1321,10 @@ func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email, } } } + // Check delivery headers for auto-forwarded emails + if !matched { + matched = deliveryHeadersMatch(msg, deliveryHeaderSection, fetchEmail) + } if !matched { continue