mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-02 13:03:35 +00:00
fix(email/imap): Properly extract all HTML parts to handle Apple Mail parsing quirks
Resolved issues where Apple Mail: - Split HTML content across MIME parts, causing rendering inconsistencies, this fix combines them. - Apple mails sends file attachments as inline......f......, leading to missing files if no Content-ID was present, this fix will treat all attachments without a Content-ID as attachments and not inline. - Set imap lookback to 48 hrs.
This commit is contained in:
@@ -64,7 +64,7 @@ func (e *Email) processMailbox(ctx context.Context, cfg IMAPConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Set value from config.
|
// TODO: Set value from config.
|
||||||
since := time.Now().Add(-24 * time.Hour)
|
since := time.Now().Add(-48 * time.Hour)
|
||||||
|
|
||||||
searchData, err := e.searchMessages(client, since)
|
searchData, err := e.searchMessages(client, since)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -235,15 +235,58 @@ func (e *Email) processEnvelope(ctx context.Context, client *imapclient.Client,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// processFullMessage processes the full email message.
|
// extractAllHTMLParts extracts all HTML parts from the given enmime part by traversing the tree.
|
||||||
|
func extractAllHTMLParts(part *enmime.Part) []string {
|
||||||
|
var htmlParts []string
|
||||||
|
|
||||||
|
// Check current part
|
||||||
|
if strings.HasPrefix(part.ContentType, "text/html") && len(part.Content) > 0 {
|
||||||
|
htmlParts = append(htmlParts, string(part.Content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process children recursively
|
||||||
|
for child := part.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
childParts := extractAllHTMLParts(child)
|
||||||
|
htmlParts = append(htmlParts, childParts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlParts
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, incomingMsg models.IncomingMessage) error {
|
func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, incomingMsg models.IncomingMessage) error {
|
||||||
envelope, err := enmime.ReadEnvelope(item.Literal)
|
envelope, err := enmime.ReadEnvelope(item.Literal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.lo.Error("error parsing email envelope", "error", err, "envelope_errors", envelope.Errors)
|
e.lo.Error("error parsing email envelope", "error", err, "message_id", incomingMsg.Message.SourceID.String)
|
||||||
|
for _, err := range envelope.Errors {
|
||||||
|
e.lo.Error("error parsing email envelope. envelope_error: ", "error", err.Error(), "message_id", incomingMsg.Message.SourceID.String)
|
||||||
|
}
|
||||||
return fmt.Errorf("parsing email envelope: %w", err)
|
return fmt.Errorf("parsing email envelope: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(envelope.HTML) > 0 {
|
// Log envelope errors.
|
||||||
|
for _, err := range envelope.Errors {
|
||||||
|
e.lo.Error("error parsing email envelope", "error", err.Error(), "message_id", incomingMsg.Message.SourceID.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all HTML content by traversing the tree
|
||||||
|
var allHTML strings.Builder
|
||||||
|
if envelope.Root != nil {
|
||||||
|
htmlParts := extractAllHTMLParts(envelope.Root)
|
||||||
|
if len(htmlParts) > 0 {
|
||||||
|
allHTML.WriteString("<div>")
|
||||||
|
for _, part := range htmlParts {
|
||||||
|
allHTML.WriteString(part)
|
||||||
|
}
|
||||||
|
allHTML.WriteString("</div>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set message content - prioritize combined HTML
|
||||||
|
if allHTML.Len() > 0 {
|
||||||
|
incomingMsg.Message.Content = allHTML.String()
|
||||||
|
incomingMsg.Message.ContentType = conversation.ContentTypeHTML
|
||||||
|
e.lo.Debug("extracted HTML content from parts", "message_id", incomingMsg.Message.SourceID.String, "content", incomingMsg.Message.Content)
|
||||||
|
} else if len(envelope.HTML) > 0 {
|
||||||
incomingMsg.Message.Content = envelope.HTML
|
incomingMsg.Message.Content = envelope.HTML
|
||||||
incomingMsg.Message.ContentType = conversation.ContentTypeHTML
|
incomingMsg.Message.ContentType = conversation.ContentTypeHTML
|
||||||
} else if len(envelope.Text) > 0 {
|
} else if len(envelope.Text) > 0 {
|
||||||
@@ -251,7 +294,10 @@ func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, inc
|
|||||||
incomingMsg.Message.ContentType = conversation.ContentTypeText
|
incomingMsg.Message.ContentType = conversation.ContentTypeText
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the angle brackets from the In-Reply-To and References headers.
|
e.lo.Debug("envelope HTML content", "message_id", incomingMsg.Message.SourceID.String, "content", incomingMsg.Message.Content)
|
||||||
|
e.lo.Debug("envelope text content", "message_id", incomingMsg.Message.SourceID.String, "content", envelope.Text)
|
||||||
|
|
||||||
|
// Clean headers
|
||||||
inReplyTo := strings.ReplaceAll(strings.ReplaceAll(envelope.GetHeader("In-Reply-To"), "<", ""), ">", "")
|
inReplyTo := strings.ReplaceAll(strings.ReplaceAll(envelope.GetHeader("In-Reply-To"), "<", ""), ">", "")
|
||||||
references := strings.Fields(envelope.GetHeader("References"))
|
references := strings.Fields(envelope.GetHeader("References"))
|
||||||
for i, ref := range references {
|
for i, ref := range references {
|
||||||
@@ -261,6 +307,7 @@ func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, inc
|
|||||||
incomingMsg.Message.InReplyTo = inReplyTo
|
incomingMsg.Message.InReplyTo = inReplyTo
|
||||||
incomingMsg.Message.References = references
|
incomingMsg.Message.References = references
|
||||||
|
|
||||||
|
// Process attachments
|
||||||
for _, att := range envelope.Attachments {
|
for _, att := range envelope.Attachments {
|
||||||
incomingMsg.Message.Attachments = append(incomingMsg.Message.Attachments, attachment.Attachment{
|
incomingMsg.Message.Attachments = append(incomingMsg.Message.Attachments, attachment.Attachment{
|
||||||
Name: att.FileName,
|
Name: att.FileName,
|
||||||
@@ -271,17 +318,27 @@ func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, inc
|
|||||||
Disposition: attachment.DispositionAttachment,
|
Disposition: attachment.DispositionAttachment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process inlines - treat ones without ContentID as regular attachments
|
||||||
for _, inline := range envelope.Inlines {
|
for _, inline := range envelope.Inlines {
|
||||||
|
disposition := attachment.DispositionInline
|
||||||
|
if inline.ContentID == "" {
|
||||||
|
disposition = attachment.DispositionAttachment
|
||||||
|
}
|
||||||
|
|
||||||
incomingMsg.Message.Attachments = append(incomingMsg.Message.Attachments, attachment.Attachment{
|
incomingMsg.Message.Attachments = append(incomingMsg.Message.Attachments, attachment.Attachment{
|
||||||
Name: inline.FileName,
|
Name: inline.FileName,
|
||||||
Content: inline.Content,
|
Content: inline.Content,
|
||||||
ContentType: inline.ContentType,
|
ContentType: inline.ContentType,
|
||||||
ContentID: inline.ContentID,
|
ContentID: inline.ContentID,
|
||||||
Size: len(inline.Content),
|
Size: len(inline.Content),
|
||||||
Disposition: attachment.DispositionInline,
|
Disposition: disposition,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
e.lo.Debug("enqueuing incoming email message for inserting in DB", "message_id", incomingMsg.Message.SourceID.String, "attachments", len(envelope.Attachments), "inlines", len(envelope.Inlines))
|
|
||||||
|
e.lo.Debug("enqueuing incoming email message", "message_id", incomingMsg.Message.SourceID.String,
|
||||||
|
"attachments", len(envelope.Attachments), "inline_attachments", len(envelope.Inlines))
|
||||||
|
|
||||||
if err := e.messageStore.EnqueueIncoming(incomingMsg); err != nil {
|
if err := e.messageStore.EnqueueIncoming(incomingMsg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user