mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-05 06:23:27 +00:00
Compare commits
3 Commits
v0.7.2-alp
...
fix/empty-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
550a3fa801 | ||
|
|
6bbfbe8cf6 | ||
|
|
f9ed326d72 |
@@ -140,6 +140,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
headerAutoSubmitted,
|
headerAutoSubmitted,
|
||||||
headerAutoreply,
|
headerAutoreply,
|
||||||
headerLibredeskLoopPrevention,
|
headerLibredeskLoopPrevention,
|
||||||
|
headerMessageID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -151,6 +152,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
seqNum uint32
|
seqNum uint32
|
||||||
autoReply bool
|
autoReply bool
|
||||||
isLoop bool
|
isLoop bool
|
||||||
|
extractedMessageID string
|
||||||
}
|
}
|
||||||
var messages []msgData
|
var messages []msgData
|
||||||
|
|
||||||
@@ -185,6 +187,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
env *imap.Envelope
|
env *imap.Envelope
|
||||||
autoReply bool
|
autoReply bool
|
||||||
isLoop bool
|
isLoop bool
|
||||||
|
extractedMessageID string
|
||||||
)
|
)
|
||||||
// Process all fetch items for the current message.
|
// Process all fetch items for the current message.
|
||||||
for {
|
for {
|
||||||
@@ -215,6 +218,9 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
if isLoopMessage(envelope, inboxEmail) {
|
if isLoopMessage(envelope, inboxEmail) {
|
||||||
isLoop = true
|
isLoop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract Message-Id from raw headers as fallback for problematic Message IDs
|
||||||
|
extractedMessageID = extractMessageIDFromHeaders(envelope)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Envelope.
|
// Envelope.
|
||||||
@@ -223,12 +229,13 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if we couldn't get headers or envelope.
|
// Skip if we couldn't get the envelope.
|
||||||
if env == nil {
|
if env == nil {
|
||||||
|
e.lo.Warn("skipping message without envelope", "seq_num", msg.SeqNum, "inbox_id", e.Identifier())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
messages = append(messages, msgData{env: env, seqNum: msg.SeqNum, autoReply: autoReply, isLoop: isLoop})
|
messages = append(messages, msgData{env: env, seqNum: msg.SeqNum, autoReply: autoReply, isLoop: isLoop, extractedMessageID: extractedMessageID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now process each collected message.
|
// Now process each collected message.
|
||||||
@@ -253,7 +260,7 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process the envelope.
|
// Process the envelope.
|
||||||
if err := e.processEnvelope(ctx, client, msgData.env, msgData.seqNum, inboxID); err != nil && err != context.Canceled {
|
if err := e.processEnvelope(ctx, client, msgData.env, msgData.seqNum, inboxID, msgData.extractedMessageID); err != nil && err != context.Canceled {
|
||||||
e.lo.Error("error processing envelope", "error", err)
|
e.lo.Error("error processing envelope", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,17 +269,32 @@ func (e *Email) fetchAndProcessMessages(ctx context.Context, client *imapclient.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processEnvelope processes a single email envelope.
|
// processEnvelope processes a single email envelope.
|
||||||
func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client, env *imap.Envelope, seqNum uint32, inboxID int) error {
|
func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client, env *imap.Envelope, seqNum uint32, inboxID int, extractedMessageID string) error {
|
||||||
if len(env.From) == 0 {
|
if len(env.From) == 0 {
|
||||||
e.lo.Warn("no sender received for email", "message_id", env.MessageID)
|
e.lo.Warn("no sender received for email", "message_id", env.MessageID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var fromAddress = strings.ToLower(env.From[0].Addr())
|
var fromAddress = strings.ToLower(env.From[0].Addr())
|
||||||
|
|
||||||
|
// Determine final Message ID - prefer IMAP-parsed, fallback to raw header extraction
|
||||||
|
messageID := env.MessageID
|
||||||
|
if messageID == "" {
|
||||||
|
messageID = extractedMessageID
|
||||||
|
if messageID != "" {
|
||||||
|
e.lo.Debug("using raw header Message-ID as fallback for malformed ID", "message_id", messageID, "subject", env.Subject, "from", fromAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop message if we still don't have a valid Message ID
|
||||||
|
if messageID == "" {
|
||||||
|
e.lo.Error("dropping message: no valid Message-ID found in IMAP parsing or raw headers", "subject", env.Subject, "from", fromAddress)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the message already exists in the database; if it does, ignore it.
|
// Check if the message already exists in the database; if it does, ignore it.
|
||||||
exists, err := e.messageStore.MessageExists(env.MessageID)
|
exists, err := e.messageStore.MessageExists(messageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.lo.Error("error checking if message exists", "message_id", env.MessageID)
|
e.lo.Error("error checking if message exists", "message_id", messageID)
|
||||||
return fmt.Errorf("checking if message exists in DB: %w", err)
|
return fmt.Errorf("checking if message exists in DB: %w", err)
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
@@ -291,7 +313,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
e.lo.Debug("processing new incoming message", "message_id", env.MessageID, "subject", env.Subject, "from", fromAddress, "inbox_id", inboxID)
|
e.lo.Debug("processing new incoming message", "message_id", messageID, "subject", env.Subject, "from", fromAddress, "inbox_id", inboxID)
|
||||||
|
|
||||||
// Make contact.
|
// Make contact.
|
||||||
firstName, lastName := getContactName(env.From[0])
|
firstName, lastName := getContactName(env.From[0])
|
||||||
@@ -350,7 +372,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
|
|||||||
InboxID: inboxID,
|
InboxID: inboxID,
|
||||||
Status: models.MessageStatusReceived,
|
Status: models.MessageStatusReceived,
|
||||||
Subject: env.Subject,
|
Subject: env.Subject,
|
||||||
SourceID: null.StringFrom(env.MessageID),
|
SourceID: null.StringFrom(messageID),
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
},
|
},
|
||||||
Contact: contact,
|
Contact: contact,
|
||||||
@@ -385,7 +407,7 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fullItem, ok := fullFetchItem.(imapclient.FetchItemDataBodySection); ok {
|
if fullItem, ok := fullFetchItem.(imapclient.FetchItemDataBodySection); ok {
|
||||||
e.lo.Debug("fetching full message body", "message_id", env.MessageID)
|
e.lo.Debug("fetching full message body", "message_id", messageID)
|
||||||
return e.processFullMessage(fullItem, incomingMsg)
|
return e.processFullMessage(fullItem, incomingMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,3 +556,13 @@ func extractAllHTMLParts(part *enmime.Part) []string {
|
|||||||
|
|
||||||
return htmlParts
|
return htmlParts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractMessageIDFromHeaders extracts and cleans the Message-ID from email headers.
|
||||||
|
// This function handles problematic Message IDs by extracting them from raw headers
|
||||||
|
// and cleaning them of angle brackets and whitespace.
|
||||||
|
func extractMessageIDFromHeaders(envelope *enmime.Envelope) string {
|
||||||
|
if rawMessageID := envelope.GetHeader(headerMessageID); rawMessageID != "" {
|
||||||
|
return strings.TrimSpace(strings.Trim(rawMessageID, "<>"))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
123
internal/inbox/channel/email/imap_test.go
Normal file
123
internal/inbox/channel/email/imap_test.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/jhillyerd/enmime"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// TestGoIMAPMessageIDParsing shows how go-imap fails to parse malformed Message-IDs
|
||||||
|
// and demonstrates the fallback solution.
|
||||||
|
// go-imap uses mail.Header.MessageID() which strictly follows RFC 5322 and returns
|
||||||
|
// empty strings for Message-IDs with multiple @ symbols.
|
||||||
|
//
|
||||||
|
// This caused emails to be dropped since we require Message-IDs for deduplication.
|
||||||
|
// References:
|
||||||
|
// - https://community.mailcow.email/d/701-multiple-at-in-message-id/5
|
||||||
|
// - https://github.com/emersion/go-message/issues/154#issuecomment-1425634946
|
||||||
|
func TestGoIMAPMessageIDParsing(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
input string
|
||||||
|
expectedIMAP string
|
||||||
|
expectedFallback string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"<normal@example.com>", "normal@example.com", "normal@example.com", "normal message ID"},
|
||||||
|
{"<malformed@@example.com>", "", "malformed@@example.com", "double @ - IMAP fails, fallback works"},
|
||||||
|
{"<001c01d710db$a8137a50$f83a6ef0$@jones.smith@example.com>", "", "001c01d710db$a8137a50$f83a6ef0$@jones.smith@example.com", "mailcow-style - IMAP fails, fallback works"},
|
||||||
|
{"<test@@@domain.com>", "", "test@@@domain.com", "triple @ - IMAP fails, fallback works"},
|
||||||
|
{" <abc123@example.com> ", "abc123@example.com", "abc123@example.com", "with whitespace - both handle correctly"},
|
||||||
|
{"abc123@example.com", "", "abc123@example.com", "no angle brackets - IMAP fails, fallback works"},
|
||||||
|
{"", "", "", "empty input"},
|
||||||
|
{"<>", "", "", "empty brackets"},
|
||||||
|
{"<CAFnQjQFhY8z@mail.example.com@gateway.company.com>", "", "CAFnQjQFhY8z@mail.example.com@gateway.company.com", "gateway-style - IMAP fails, fallback works"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Test go-imap parsing behavior
|
||||||
|
var h mail.Header
|
||||||
|
h.Set("Message-Id", tc.input)
|
||||||
|
imapResult, _ := h.MessageID()
|
||||||
|
|
||||||
|
if imapResult != tc.expectedIMAP {
|
||||||
|
t.Errorf("IMAP parsing of %q: expected %q, got %q", tc.input, tc.expectedIMAP, imapResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test fallback solution
|
||||||
|
if tc.input != "" {
|
||||||
|
rawEmail := "From: test@example.com\nMessage-ID: " + tc.input + "\n\nBody"
|
||||||
|
envelope, err := enmime.ReadEnvelope(strings.NewReader(rawEmail))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackResult := extractMessageIDFromHeaders(envelope)
|
||||||
|
if fallbackResult != tc.expectedFallback {
|
||||||
|
t.Errorf("Fallback extraction of %q: expected %q, got %q", tc.input, tc.expectedFallback, fallbackResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical check: ensure fallback works when IMAP fails
|
||||||
|
if imapResult == "" && tc.expectedFallback != "" && fallbackResult == "" {
|
||||||
|
t.Errorf("CRITICAL: Both IMAP and fallback failed for %q - would drop email!", tc.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TestEdgeCasesMessageID tests additional edge cases for Message-ID extraction.
|
||||||
|
func TestEdgeCasesMessageID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no Message-ID header",
|
||||||
|
email: `From: test@example.com
|
||||||
|
To: inbox@test.com
|
||||||
|
Subject: Test
|
||||||
|
|
||||||
|
Body`,
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed header syntax",
|
||||||
|
email: `From: test@example.com
|
||||||
|
Message-ID: malformed-no-brackets@@domain.com
|
||||||
|
To: inbox@test.com
|
||||||
|
|
||||||
|
Body`,
|
||||||
|
expected: "malformed-no-brackets@@domain.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple Message-ID headers (first wins)",
|
||||||
|
email: `From: test@example.com
|
||||||
|
Message-ID: <first@example.com>
|
||||||
|
Message-ID: <second@@example.com>
|
||||||
|
To: inbox@test.com
|
||||||
|
|
||||||
|
Body`,
|
||||||
|
expected: "first@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
envelope, err := enmime.ReadEnvelope(strings.NewReader(tt.email))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := extractMessageIDFromHeaders(envelope)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user