Skip to content

Commit 9a65170

Browse files
committed
fix[imap]: skip auto reply email messages
Fixes #94
1 parent a0203f8 commit 9a65170

File tree

1 file changed

+82
-34
lines changed
  • internal/inbox/channel/email

1 file changed

+82
-34
lines changed

internal/inbox/channel/email/imap.go

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ func (e *Email) processMailbox(ctx context.Context, scanInboxSince time.Duration
7676
case "none":
7777
client, err = imapclient.DialInsecure(address, imapOptions)
7878
case "starttls":
79-
fmt.Println("starttls")
80-
fmt.Println("skip verify", cfg.TLSSkipVerify)
81-
fmt.Println(address)
8279
client, err = imapclient.DialStartTLS(address, imapOptions)
8380
case "tls":
8481
client, err = imapclient.DialTLS(address, imapOptions)
@@ -132,13 +129,21 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
132129
seqSet := imap.SeqSet{}
133130
seqSet.AddRange(searchResults.Min, searchResults.Max)
134131

135-
// Fetch only envelope, body is fetch later if the message is new.
132+
// Fetch envelope and headers needed for auto-reply detection.
136133
fetchOptions := &imap.FetchOptions{
137134
Envelope: true,
135+
BodySection: []*imap.FetchItemBodySection{
136+
{
137+
Specifier: imap.PartSpecifierHeader,
138+
HeaderFields: []string{
139+
"Auto-Submitted",
140+
"X-Autoreply",
141+
},
142+
},
143+
},
138144
}
139145

140146
fetchCmd := client.Fetch(seqSet, fetchOptions)
141-
142147
for {
143148
// Check for context cancellation before fetching the next message.
144149
select {
@@ -154,7 +159,12 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
154159
return nil
155160
}
156161

157-
// Process message envelope.
162+
var (
163+
env *imap.Envelope
164+
autoReply bool
165+
)
166+
167+
// Process all fetch items for the current message.
158168
for {
159169
// Check for context cancellation before processing the next item.
160170
select {
@@ -164,32 +174,57 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
164174
}
165175

166176
// Fetch the next item in the message.
167-
fetchItem := msg.Next()
168-
if fetchItem == nil {
177+
item := msg.Next()
178+
if item == nil {
169179
// No message items left to process.
170180
break
171181
}
172182

173-
// Process the envelope item.
174-
if item, ok := fetchItem.(imapclient.FetchItemDataEnvelope); ok {
175-
if err := e.processEnvelope(ctx, client, item.Envelope, msg.SeqNum, inboxID); err != nil && err != context.Canceled {
176-
e.lo.Error("error processing envelope", "error", err)
183+
// Body section.
184+
if bs, ok := item.(imapclient.FetchItemDataBodySection); ok && bs.Literal != nil {
185+
envelope, err := enmime.ReadEnvelope(bs.Literal)
186+
if err != nil {
187+
e.lo.Error("error reading envelope", "error", err)
188+
continue
189+
}
190+
if isAutoReply(envelope) {
191+
autoReply = true
177192
}
178193
}
194+
195+
// Envelope.
196+
if ed, ok := item.(imapclient.FetchItemDataEnvelope); ok {
197+
env = ed.Envelope
198+
}
199+
}
200+
201+
// Skip if we couldn't get headers or envelope.
202+
if env == nil {
203+
continue
204+
}
205+
206+
// Skip if this is an auto-reply message.
207+
if autoReply {
208+
e.lo.Info("skipping auto-reply message", "subject", env.Subject, "message_id", env.MessageID)
209+
continue
210+
}
211+
212+
// Process the envelope.
213+
if err := e.processEnvelope(ctx, client, env, msg.SeqNum, inboxID); err != nil && err != context.Canceled {
214+
e.lo.Error("error processing envelope", "error", err)
179215
}
180216
}
181217
}
182218

183-
// processEnvelope processes an email envelope.
219+
// processEnvelope processes a single email envelope.
184220
func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client, env *imap.Envelope, seqNum uint32, inboxID int) error {
185221
if len(env.From) == 0 {
186222
e.lo.Warn("no sender received for email", "message_id", env.MessageID)
187223
return nil
188224
}
189225
var fromAddress = strings.ToLower(env.From[0].Addr())
190226

191-
// Check if the message already exists in the database.
192-
// If it does, ignore it.
227+
// Check if the message already exists in the database; if it does, ignore it.
193228
exists, err := e.messageStore.MessageExists(env.MessageID)
194229
if err != nil {
195230
e.lo.Error("error checking if message exists", "message_id", env.MessageID)
@@ -211,7 +246,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
211246
return nil
212247
}
213248

214-
e.lo.Debug("message does not exist", "message_id", env.MessageID)
249+
e.lo.Debug("processing new incoming message", "message_id", env.MessageID, "subject", env.Subject, "from", fromAddress, "inbox_id", inboxID)
215250

216251
// Make contact.
217252
firstName, lastName := getContactName(env.From[0])
@@ -277,6 +312,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
277312
InboxID: inboxID,
278313
}
279314

315+
// Fetch full message body.
280316
fetchOptions := &imap.FetchOptions{
281317
BodySection: []*imap.FetchItemBodySection{{}},
282318
}
@@ -310,24 +346,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
310346
}
311347
}
312348

313-
// extractAllHTMLParts extracts all HTML parts from the given enmime part by traversing the tree.
314-
func extractAllHTMLParts(part *enmime.Part) []string {
315-
var htmlParts []string
316-
317-
// Check current part
318-
if strings.HasPrefix(part.ContentType, "text/html") && len(part.Content) > 0 {
319-
htmlParts = append(htmlParts, string(part.Content))
320-
}
321-
322-
// Process children recursively
323-
for child := part.FirstChild; child != nil; child = child.NextSibling {
324-
childParts := extractAllHTMLParts(child)
325-
htmlParts = append(htmlParts, childParts...)
326-
}
327-
328-
return htmlParts
329-
}
330-
349+
// processFullMessage processes the full message and enqueues it for inserting into the database.
331350
func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, incomingMsg models.IncomingMessage) error {
332351
envelope, err := enmime.ReadEnvelope(item.Literal)
333352
if err != nil {
@@ -432,3 +451,32 @@ func getContactName(imapAddr imap.Address) (string, string) {
432451
}
433452
return names[0], names[1]
434453
}
454+
455+
// isAutoReply checks if a given email envelope indicates an auto-reply message.
456+
func isAutoReply(envelope *enmime.Envelope) bool {
457+
if as := strings.ToLower(strings.TrimSpace(envelope.GetHeader("Auto-Submitted"))); as != "" && as != "no" {
458+
return true
459+
}
460+
if strings.TrimSpace(envelope.GetHeader("X-Autoreply")) != "" {
461+
return true
462+
}
463+
return false
464+
}
465+
466+
// extractAllHTMLParts extracts all HTML parts from the given enmime part by traversing the tree.
467+
func extractAllHTMLParts(part *enmime.Part) []string {
468+
var htmlParts []string
469+
470+
// Check current part
471+
if strings.HasPrefix(part.ContentType, "text/html") && len(part.Content) > 0 {
472+
htmlParts = append(htmlParts, string(part.Content))
473+
}
474+
475+
// Process children recursively
476+
for child := part.FirstChild; child != nil; child = child.NextSibling {
477+
childParts := extractAllHTMLParts(child)
478+
htmlParts = append(htmlParts, childParts...)
479+
}
480+
481+
return htmlParts
482+
}

0 commit comments

Comments
 (0)