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
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:
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user