From 23f6202dfd53333c953853a52095ffccb8b98c88 Mon Sep 17 00:00:00 2001 From: butschster Date: Tue, 31 Mar 2026 21:13:59 +0400 Subject: [PATCH] fix: decode RFC 2047 encoded-word in SMTP subject headers SMTP subject headers containing non-ASCII characters arrive as MIME encoded-words (e.g. =?utf-8?Q?...?=). These were stored as-is, appearing garbled in the UI. Use mime.WordDecoder to decode them. --- modules/smtp/handler.go | 9 +++++++- modules/smtp/handler_test.go | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/modules/smtp/handler.go b/modules/smtp/handler.go index c189797..7781723 100644 --- a/modules/smtp/handler.go +++ b/modules/smtp/handler.go @@ -199,9 +199,16 @@ func parseEmail(raw []byte, recipients []string) (*ParsedEmail, []parsedAttachme bcc = []string{} } + // Decode RFC 2047 encoded-word in Subject header (e.g. =?utf-8?Q?...?=). + subject := msg.Header.Get("Subject") + dec := new(mime.WordDecoder) + if decoded, err := dec.DecodeHeader(subject); err == nil { + subject = decoded + } + parsed := &ParsedEmail{ ID: msgID, - Subject: msg.Header.Get("Subject"), + Subject: subject, Raw: string(raw), From: parseAddresses(msg.Header, "From"), To: to, diff --git a/modules/smtp/handler_test.go b/modules/smtp/handler_test.go index 22ea2d6..72646f8 100644 --- a/modules/smtp/handler_test.go +++ b/modules/smtp/handler_test.go @@ -165,6 +165,48 @@ func TestParseAddresses(t *testing.T) { }) } +func TestParseEmail_RFC2047Subject(t *testing.T) { + tests := []struct { + name string + subject string + want string + }{ + { + name: "Q-encoded UTF-8", + subject: "=?utf-8?Q?Handled_with_Care_by_Chuck_Norris=E2=80=94Your_Package?=", + want: "Handled with Care by Chuck Norris\u2014Your Package", + }, + { + name: "B-encoded UTF-8", + subject: "=?utf-8?B?SGVsbG8gV29ybGQ=?=", + want: "Hello World", + }, + { + name: "mixed encoded and plain", + subject: "Handled with Care by Chuck =?utf-8?Q?Norris=E2=80=94Your?= Package Is Shipped!", + want: "Handled with Care by Chuck Norris\u2014Your Package Is Shipped!", + }, + { + name: "plain subject unchanged", + subject: "Plain Subject", + want: "Plain Subject", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + raw := []byte("From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: " + tt.subject + "\r\nContent-Type: text/plain\r\n\r\ntest body") + parsed, _, err := parseEmail(raw, []string{"recipient@example.com"}) + if err != nil { + t.Fatal(err) + } + if parsed.Subject != tt.want { + t.Errorf("Subject = %q, want %q", parsed.Subject, tt.want) + } + }) + } +} + func TestPreviewMapper(t *testing.T) { m := &previewMapper{}