From 50b7009603a2418820a2e7f998720720170594dc Mon Sep 17 00:00:00 2001
From: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Date: Wed, 1 May 2024 12:19:38 +0000
Subject: [PATCH] [v7.0/forgejo] Add inline attachments to comments and prevent
 double handling of mails (#3566)

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/3504

If incoming email is configured and an email is sent, inline attachments are currently not added to the comment if it has the `Content-Disposition: inline` instead of `Content-Disposition: attachment` as e.g. with Apple Mail.

This adds inline attachments (`Content-Disposition: inline`) that *have a filename* as attachment to the comment. Other elements with `Content-Disposition: inline` are not attached as attachment to the comment.

In addition, a check has been added to prevent mails from being processed twice.

Fixes #3496

Co-authored-by: Beowulf <beowulf@beocode.eu>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3566
Reviewed-by: Beowulf <beowulf@beocode.eu>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
---
 release-notes/8.0.0/fix/3504.md           |  1 +
 services/mailer/incoming/incoming.go      | 18 ++++++++++
 services/mailer/incoming/incoming_test.go | 40 +++++++++++++++++++++++
 3 files changed, 59 insertions(+)
 create mode 100644 release-notes/8.0.0/fix/3504.md

diff --git a/release-notes/8.0.0/fix/3504.md b/release-notes/8.0.0/fix/3504.md
new file mode 100644
index 0000000000..8ece1557ab
--- /dev/null
+++ b/release-notes/8.0.0/fix/3504.md
@@ -0,0 +1 @@
+Fixed that inline attachments of emails (as they occur for example with Apple Mail) are not attached to comments.
diff --git a/services/mailer/incoming/incoming.go b/services/mailer/incoming/incoming.go
index eade0cf271..6530b7cc60 100644
--- a/services/mailer/incoming/incoming.go
+++ b/services/mailer/incoming/incoming.go
@@ -219,6 +219,11 @@ loop:
 			}
 
 			err := func() error {
+				if isAlreadyHandled(handledSet, msg) {
+					log.Debug("Skipping already handled message")
+					return nil
+				}
+
 				r := msg.GetBody(section)
 				if r == nil {
 					return fmt.Errorf("could not get body from message: %w", err)
@@ -277,6 +282,11 @@ loop:
 	return nil
 }
 
+// isAlreadyHandled tests if the message was already handled
+func isAlreadyHandled(handledSet *imap.SeqSet, msg *imap.Message) bool {
+	return handledSet.Contains(msg.SeqNum)
+}
+
 // isAutomaticReply tests if the headers indicate an automatic reply
 func isAutomaticReply(env *enmime.Envelope) bool {
 	autoSubmitted := env.GetHeader("Auto-Submitted")
@@ -367,6 +377,14 @@ func getContentFromMailReader(env *enmime.Envelope) *MailContent {
 			Content: attachment.Content,
 		})
 	}
+	for _, inline := range env.Inlines {
+		if inline.FileName != "" {
+			attachments = append(attachments, &Attachment{
+				Name:    inline.FileName,
+				Content: inline.Content,
+			})
+		}
+	}
 
 	return &MailContent{
 		Content:     reply.FromText(env.Text),
diff --git a/services/mailer/incoming/incoming_test.go b/services/mailer/incoming/incoming_test.go
index 5d84848e3f..f2bb7fc498 100644
--- a/services/mailer/incoming/incoming_test.go
+++ b/services/mailer/incoming/incoming_test.go
@@ -7,10 +7,24 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/emersion/go-imap"
 	"github.com/jhillyerd/enmime"
 	"github.com/stretchr/testify/assert"
 )
 
+func TestNotHandleTwice(t *testing.T) {
+	handledSet := new(imap.SeqSet)
+	msg := imap.NewMessage(90, []imap.FetchItem{imap.FetchBody})
+
+	handled := isAlreadyHandled(handledSet, msg)
+	assert.Equal(t, false, handled)
+
+	handledSet.AddNum(msg.SeqNum)
+
+	handled = isAlreadyHandled(handledSet, msg)
+	assert.Equal(t, true, handled)
+}
+
 func TestIsAutomaticReply(t *testing.T) {
 	cases := []struct {
 		Headers  map[string]string
@@ -95,6 +109,32 @@ func TestGetContentFromMailReader(t *testing.T) {
 	assert.Equal(t, "attachment.txt", content.Attachments[0].Name)
 	assert.Equal(t, []byte("attachment content"), content.Attachments[0].Content)
 
+	mailString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
+		"\r\n" +
+		"--message-boundary\r\n" +
+		"Content-Type: multipart/alternative; boundary=text-boundary\r\n" +
+		"\r\n" +
+		"--text-boundary\r\n" +
+		"Content-Type: text/plain\r\n" +
+		"Content-Disposition: inline\r\n" +
+		"\r\n" +
+		"mail content\r\n" +
+		"--text-boundary--\r\n" +
+		"--message-boundary\r\n" +
+		"Content-Type: text/plain\r\n" +
+		"Content-Disposition: inline; filename=attachment.txt\r\n" +
+		"\r\n" +
+		"attachment content\r\n" +
+		"--message-boundary--\r\n"
+
+	env, err = enmime.ReadEnvelope(strings.NewReader(mailString))
+	assert.NoError(t, err)
+	content = getContentFromMailReader(env)
+	assert.Equal(t, "mail content", content.Content)
+	assert.Len(t, content.Attachments, 1)
+	assert.Equal(t, "attachment.txt", content.Attachments[0].Name)
+	assert.Equal(t, []byte("attachment content"), content.Attachments[0].Content)
+
 	mailString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
 		"\r\n" +
 		"--message-boundary\r\n" +