Switch IMAP library from go-imap/v2 to v1 for Outlook compatibility
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m16s

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 14:16:04 +01:00
parent 1e22bacdc9
commit 3d97ccf38c
3 changed files with 62 additions and 66 deletions

View File

@@ -5,7 +5,8 @@ go 1.25
require ( require (
github.com/99designs/gqlgen v0.17.88 github.com/99designs/gqlgen v0.17.88
github.com/anthropics/anthropic-sdk-go v1.26.0 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/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
@@ -23,7 +24,6 @@ require (
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // 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/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect

View File

@@ -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/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 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 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 v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap/v2 v2.0.0-beta.8/go.mod h1:dhoFe2Q0PwLrMD7oZw8ODuaD0vLYPe5uj2wcOMnvh48= 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 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= 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 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 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.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.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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=

View File

@@ -6,8 +6,9 @@ import (
"log" "log"
"time" "time"
"github.com/emersion/go-imap/v2" "github.com/emersion/go-imap"
"github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
) )
type IMAPConfig struct { type IMAPConfig struct {
@@ -21,91 +22,86 @@ type IMAPConfig struct {
func (s *EmailSyncService) fetchEmailsIMAP(since time.Time) ([]graphMessage, error) { func (s *EmailSyncService) fetchEmailsIMAP(since time.Time) ([]graphMessage, error) {
addr := fmt.Sprintf("%s:%s", s.Config.IMAP.Host, s.Config.IMAP.Port) 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 { if err != nil {
return nil, fmt.Errorf("IMAP connect: %w", err) 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) 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) return nil, fmt.Errorf("IMAP select INBOX: %w", err)
} }
// Search for emails since the given time // Search for emails since the given time
sinceDate := since.UTC() criteria := imap.NewSearchCriteria()
searchCriteria := &imap.SearchCriteria{ criteria.Since = since.UTC()
Since: sinceDate,
} seqNums, err := c.Search(criteria)
searchData, err := c.Search(searchCriteria, nil).Wait()
if err != nil { if err != nil {
return nil, fmt.Errorf("IMAP search: %w", err) return nil, fmt.Errorf("IMAP search: %w", err)
} }
if searchData.AllSeqNums() == nil || len(searchData.AllSeqNums()) == 0 { if len(seqNums) == 0 {
return nil, nil return nil, nil
} }
seqNums := searchData.AllSeqNums() log.Printf("[EmailSync/IMAP] Found %d messages since %s", len(seqNums), since.UTC().Format(time.RFC3339))
log.Printf("[EmailSync/IMAP] Found %d messages since %s", len(seqNums), sinceDate.Format(time.RFC3339))
seqSet := imap.SeqSetNum(seqNums...) seqSet := new(imap.SeqSet)
fetchOptions := &imap.FetchOptions{ seqSet.AddNum(seqNums...)
Envelope: true,
BodySection: []*imap.FetchItemBodySection{{}},
}
fetchCmd := c.Fetch(seqSet, fetchOptions) section := &imap.BodySectionName{}
defer fetchCmd.Close() 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 var messages []graphMessage
for { for msg := range msgChan {
msg := fetchCmd.Next() if msg.Envelope == nil {
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 {
continue continue
} }
// Convert to graphMessage format for unified processing
var fromName, fromAddr string var fromName, fromAddr string
if len(envelope.From) > 0 { if len(msg.Envelope.From) > 0 {
fromName = envelope.From[0].Name fromName = msg.Envelope.From[0].PersonalName
fromAddr = envelope.From[0].Addr() 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{ gm := graphMessage{
ID: envelope.MessageID, ID: msg.Envelope.MessageId,
Subject: envelope.Subject, Subject: msg.Envelope.Subject,
ReceivedDateTime: envelope.Date.Format(time.RFC3339), ReceivedDateTime: msg.Envelope.Date.Format(time.RFC3339),
From: graphFrom{ From: graphFrom{
EmailAddress: graphEmailAddress{ EmailAddress: graphEmailAddress{
Name: fromName, Name: fromName,
@@ -121,13 +117,9 @@ func (s *EmailSyncService) fetchEmailsIMAP(since time.Time) ([]graphMessage, err
messages = append(messages, gm) messages = append(messages, gm)
} }
if err := fetchCmd.Close(); err != nil { if err := <-done; err != nil {
return nil, fmt.Errorf("IMAP fetch: %w", err) 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 return messages, nil
} }