From 53d5715429e8f19d269a7facae53a54acaaee8ca Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 8 Jun 2025 02:22:36 +0530 Subject: [PATCH] fix: implement loop prevention header in emails #Ref 90 --- internal/inbox/channel/email/imap.go | 35 +++++++++++++++++++++++++--- internal/inbox/channel/email/smtp.go | 21 +++++++++++++---- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/internal/inbox/channel/email/imap.go b/internal/inbox/channel/email/imap.go index a6b01c5..bccb7f6 100644 --- a/internal/inbox/channel/email/imap.go +++ b/internal/inbox/channel/email/imap.go @@ -11,6 +11,7 @@ import ( "github.com/abhinavxd/libredesk/internal/attachment" "github.com/abhinavxd/libredesk/internal/conversation/models" "github.com/abhinavxd/libredesk/internal/envelope" + "github.com/abhinavxd/libredesk/internal/stringutil" umodels "github.com/abhinavxd/libredesk/internal/user/models" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" @@ -136,8 +137,9 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. { Specifier: imap.PartSpecifierHeader, HeaderFields: []string{ - "Auto-Submitted", - "X-Autoreply", + headerAutoSubmitted, + headerAutoreply, + headerLibredeskLoopPrevention, }, }, }, @@ -148,10 +150,18 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. env *imap.Envelope seqNum uint32 autoReply bool + isLoop bool } var messages []msgData fetchCmd := client.Fetch(seqSet, fetchOptions) + + // Extract the inbox email address. + inboxEmail, err := stringutil.ExtractEmail(e.FromAddress()) + if err != nil { + e.lo.Error("failed to extract email address from the 'From' header", "error", err) + return fmt.Errorf("failed to extract email address from 'From' header: %w", err) + } for { // Check for context cancellation before fetching the next message. select { @@ -170,6 +180,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. var ( env *imap.Envelope autoReply bool + isLoop bool ) // Process all fetch items for the current message. for { @@ -197,6 +208,9 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. if isAutoReply(envelope) { autoReply = true } + if isLoopMessage(envelope, inboxEmail) { + isLoop = true + } } // Envelope. @@ -210,7 +224,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. continue } - messages = append(messages, msgData{env: env, seqNum: msg.SeqNum, autoReply: autoReply}) + messages = append(messages, msgData{env: env, seqNum: msg.SeqNum, autoReply: autoReply, isLoop: isLoop}) } // Now process each collected message. @@ -228,6 +242,12 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient. continue } + // Skip if this message is a loop prevention message. + if msgData.isLoop { + e.lo.Info("skipping message with loop prevention header", "subject", msgData.env.Subject, "message_id", msgData.env.MessageID) + continue + } + // Process the envelope. if err := e.processEnvelope(ctx, client, msgData.env, msgData.seqNum, inboxID); err != nil && err != context.Canceled { e.lo.Error("error processing envelope", "error", err) @@ -484,6 +504,15 @@ func isAutoReply(envelope *enmime.Envelope) bool { return false } +// isLoopMessage returns true if the email is a loop prevention message. i.e., it has the `X-Libredesk-Loop-Prevention` header with the inbox email address. +func isLoopMessage(envelope *enmime.Envelope, inboxEmailaddress string) bool { + loopHeader := envelope.GetHeader(headerLibredeskLoopPrevention) + if loopHeader == "" { + return false + } + return strings.EqualFold(loopHeader, inboxEmailaddress) +} + // extractAllHTMLParts extracts all HTML parts from the given enmime part by traversing the tree. func extractAllHTMLParts(part *enmime.Part) []string { var htmlParts []string diff --git a/internal/inbox/channel/email/smtp.go b/internal/inbox/channel/email/smtp.go index 5d191c8..c2beafc 100644 --- a/internal/inbox/channel/email/smtp.go +++ b/internal/inbox/channel/email/smtp.go @@ -8,14 +8,19 @@ import ( "net/textproto" "github.com/abhinavxd/libredesk/internal/conversation/models" + "github.com/abhinavxd/libredesk/internal/stringutil" "github.com/knadh/smtppool" ) const ( - headerReturnPath = "Return-Path" - headerMessageID = "Message-ID" - headerReferences = "References" - headerInReplyTo = "In-Reply-To" + headerReturnPath = "Return-Path" + headerMessageID = "Message-ID" + headerReferences = "References" + headerInReplyTo = "In-Reply-To" + headerLibredeskLoopPrevention = "X-Libredesk-Loop-Prevention" + headerAutoreply = "X-Autoreply" + headerAutoSubmitted = "Auto-Submitted" + dispositionInline = "inline" ) @@ -102,6 +107,14 @@ func (e *Email) Send(m models.Message) error { Headers: textproto.MIMEHeader{}, } + // Set libredesk loop prevention header to from address. + emailAddress, err := stringutil.ExtractEmail(m.From) + if err != nil { + e.lo.Error("Failed to extract email address from the 'From' header", "error", err) + return fmt.Errorf("failed to extract email address from 'From' header: %w", err) + } + email.Headers.Set(headerLibredeskLoopPrevention, emailAddress) + // Attach SMTP level headers for key, value := range e.headers { email.Headers.Set(key, value)