From 3d97ccf38c365826a31e9dad5e557027fb5c590e Mon Sep 17 00:00:00 2001 From: Adam French Date: Wed, 15 Apr 2026 14:16:04 +0100 Subject: [PATCH] Switch IMAP library from go-imap/v2 to v1 for Outlook compatibility go-imap/v2's strict wire parser rejects Outlook's non-standard IMAP login responses. v1 is more lenient and handles these gracefully. Co-Authored-By: Claude Opus 4.6 --- backend/go.mod | 4 +- backend/go.sum | 8 ++- backend/services/email_imap.go | 116 +++++++++++++++------------------ 3 files changed, 62 insertions(+), 66 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index a520ab3..59ee4a9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,7 +5,8 @@ go 1.25 require ( github.com/99designs/gqlgen v0.17.88 github.com/anthropics/anthropic-sdk-go v1.26.0 - github.com/emersion/go-imap/v2 v2.0.0-beta.8 + github.com/emersion/go-imap v1.2.1 + github.com/emersion/go-message v0.18.2 github.com/gin-gonic/gin v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/gorilla/websocket v1.5.3 @@ -23,7 +24,6 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/emersion/go-message v0.18.2 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index b8391f0..b33ebea 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -66,12 +66,15 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7c github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/emersion/go-imap/v2 v2.0.0-beta.8 h1:5IXZK1E33DyeP526320J3RS7eFlCYGFgtbrfapqDPug= -github.com/emersion/go-imap/v2 v2.0.0-beta.8/go.mod h1:dhoFe2Q0PwLrMD7oZw8ODuaD0vLYPe5uj2wcOMnvh48= +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -394,6 +397,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/backend/services/email_imap.go b/backend/services/email_imap.go index 985c13a..0245c60 100644 --- a/backend/services/email_imap.go +++ b/backend/services/email_imap.go @@ -6,8 +6,9 @@ import ( "log" "time" - "github.com/emersion/go-imap/v2" - "github.com/emersion/go-imap/v2/imapclient" + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-message/mail" ) type IMAPConfig struct { @@ -21,91 +22,86 @@ type IMAPConfig struct { func (s *EmailSyncService) fetchEmailsIMAP(since time.Time) ([]graphMessage, error) { addr := fmt.Sprintf("%s:%s", s.Config.IMAP.Host, s.Config.IMAP.Port) - c, err := imapclient.DialTLS(addr, nil) + c, err := client.DialTLS(addr, nil) if err != nil { return nil, fmt.Errorf("IMAP connect: %w", err) } - defer c.Close() + defer c.Logout() - if err := c.Login(s.Config.IMAP.Email, s.Config.IMAP.Password).Wait(); err != nil { + if err := c.Login(s.Config.IMAP.Email, s.Config.IMAP.Password); err != nil { return nil, fmt.Errorf("IMAP login: %w", err) } - if _, err := c.Select("INBOX", nil).Wait(); err != nil { + _, err = c.Select("INBOX", false) + if err != nil { return nil, fmt.Errorf("IMAP select INBOX: %w", err) } // Search for emails since the given time - sinceDate := since.UTC() - searchCriteria := &imap.SearchCriteria{ - Since: sinceDate, - } - searchData, err := c.Search(searchCriteria, nil).Wait() + criteria := imap.NewSearchCriteria() + criteria.Since = since.UTC() + + seqNums, err := c.Search(criteria) if err != nil { return nil, fmt.Errorf("IMAP search: %w", err) } - if searchData.AllSeqNums() == nil || len(searchData.AllSeqNums()) == 0 { + if len(seqNums) == 0 { return nil, nil } - seqNums := searchData.AllSeqNums() - log.Printf("[EmailSync/IMAP] Found %d messages since %s", len(seqNums), sinceDate.Format(time.RFC3339)) + log.Printf("[EmailSync/IMAP] Found %d messages since %s", len(seqNums), since.UTC().Format(time.RFC3339)) - seqSet := imap.SeqSetNum(seqNums...) - fetchOptions := &imap.FetchOptions{ - Envelope: true, - BodySection: []*imap.FetchItemBodySection{{}}, - } + seqSet := new(imap.SeqSet) + seqSet.AddNum(seqNums...) - fetchCmd := c.Fetch(seqSet, fetchOptions) - defer fetchCmd.Close() + section := &imap.BodySectionName{} + items := []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()} + + msgChan := make(chan *imap.Message, len(seqNums)) + done := make(chan error, 1) + go func() { + done <- c.Fetch(seqSet, items, msgChan) + }() var messages []graphMessage - for { - msg := fetchCmd.Next() - if msg == nil { - break - } - - var envelope *imap.Envelope - var bodyContent string - - for { - item := msg.Next() - if item == nil { - break - } - - switch data := item.(type) { - case imapclient.FetchItemDataEnvelope: - envelope = data.Envelope - case imapclient.FetchItemDataBodySection: - body, err := io.ReadAll(data.Literal) - if err != nil { - log.Printf("[EmailSync/IMAP] Error reading body: %v", err) - continue - } - bodyContent = string(body) - } - } - - if envelope == nil { + for msg := range msgChan { + if msg.Envelope == nil { continue } - // Convert to graphMessage format for unified processing var fromName, fromAddr string - if len(envelope.From) > 0 { - fromName = envelope.From[0].Name - fromAddr = envelope.From[0].Addr() + if len(msg.Envelope.From) > 0 { + fromName = msg.Envelope.From[0].PersonalName + fromAddr = msg.Envelope.From[0].Address() + } + + var bodyContent string + bodyReader := msg.GetBody(section) + if bodyReader != nil { + mr, err := mail.CreateReader(bodyReader) + if err == nil { + for { + p, err := mr.NextPart() + if err != nil { + break + } + switch p.Header.(type) { + case *mail.InlineHeader: + b, err := io.ReadAll(p.Body) + if err == nil { + bodyContent = string(b) + } + } + } + } } gm := graphMessage{ - ID: envelope.MessageID, - Subject: envelope.Subject, - ReceivedDateTime: envelope.Date.Format(time.RFC3339), + ID: msg.Envelope.MessageId, + Subject: msg.Envelope.Subject, + ReceivedDateTime: msg.Envelope.Date.Format(time.RFC3339), From: graphFrom{ EmailAddress: graphEmailAddress{ Name: fromName, @@ -121,13 +117,9 @@ func (s *EmailSyncService) fetchEmailsIMAP(since time.Time) ([]graphMessage, err messages = append(messages, gm) } - if err := fetchCmd.Close(); err != nil { + if err := <-done; err != nil { return nil, fmt.Errorf("IMAP fetch: %w", err) } - if err := c.Logout().Wait(); err != nil { - log.Printf("[EmailSync/IMAP] Logout error: %v", err) - } - return messages, nil }