From bad15c61067c2916f4b74c468050efe902f47785 Mon Sep 17 00:00:00 2001 From: prdai Date: Sat, 30 May 2026 23:01:16 +0530 Subject: [PATCH 1/4] feat: update extractAllHeaders func to only use the header section for header extraction within the implementation, we are first cloning the rawMessage that is provided because the rawMessage and rawHeaderMessage would other wise share the same memory address, afterwards we utilize the strings.Index to find at which point the character occur in this scneario "\r\n\r\n" or "\n\n", and then we update the rawHeaderMessage, we default the entire rawMessage just in case --- internal/delivery/parser/parser.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/delivery/parser/parser.go b/internal/delivery/parser/parser.go index a828062..fd04a24 100644 --- a/internal/delivery/parser/parser.go +++ b/internal/delivery/parser/parser.go @@ -1181,8 +1181,14 @@ func ReadDataCommand(r *bufio.Reader, maxSize int64) ([]byte, error) { // multi-line continuations. Only lines starting with space/tab extend the previous header, // preventing malformed headers from corrupting adjacent fields. func extractAllHeaders(rawMessage string) []MessageHeader { + rawHeaderMessage := strings.Clone(rawMessage); + if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd > 0 { + rawHeaderMessage = rawHeaderMessage[:headerEnd] + } else if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd > 0 { + rawHeaderMessage = rawHeaderMessage[:headerEnd] + } var headers []MessageHeader - lines := strings.Split(rawMessage, "\n") + lines := strings.Split(rawHeaderMessage, "\n") sequence := 0 var currentHeaderName string var currentHeaderValue strings.Builder From ec96cdbca079cf6d257c689cc9e3bdde7f927578 Mon Sep 17 00:00:00 2001 From: Ranuga <79456372+prdai@users.noreply.github.com> Date: Sun, 31 May 2026 00:34:28 +0530 Subject: [PATCH 2/4] Update internal/delivery/parser/parser.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- internal/delivery/parser/parser.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/delivery/parser/parser.go b/internal/delivery/parser/parser.go index fd04a24..c094b53 100644 --- a/internal/delivery/parser/parser.go +++ b/internal/delivery/parser/parser.go @@ -1181,12 +1181,12 @@ func ReadDataCommand(r *bufio.Reader, maxSize int64) ([]byte, error) { // multi-line continuations. Only lines starting with space/tab extend the previous header, // preventing malformed headers from corrupting adjacent fields. func extractAllHeaders(rawMessage string) []MessageHeader { - rawHeaderMessage := strings.Clone(rawMessage); - if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd > 0 { - rawHeaderMessage = rawHeaderMessage[:headerEnd] - } else if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd > 0 { - rawHeaderMessage = rawHeaderMessage[:headerEnd] - } +rawHeaderMessage := rawMessage +if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd >= 0 { + rawHeaderMessage = rawMessage[:headerEnd] +} else if headerEnd := strings.Index(rawMessage, "\n\n"); headerEnd >= 0 { + rawHeaderMessage = rawMessage[:headerEnd] +} var headers []MessageHeader lines := strings.Split(rawHeaderMessage, "\n") sequence := 0 From 6e6fdac11ea80287a959b1568028881e87c99b10 Mon Sep 17 00:00:00 2001 From: prdai Date: Thu, 11 Jun 2026 06:23:14 +0530 Subject: [PATCH 3/4] chore: format parser.go --- internal/delivery/parser/parser.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/delivery/parser/parser.go b/internal/delivery/parser/parser.go index c094b53..64d69bc 100644 --- a/internal/delivery/parser/parser.go +++ b/internal/delivery/parser/parser.go @@ -1181,12 +1181,12 @@ func ReadDataCommand(r *bufio.Reader, maxSize int64) ([]byte, error) { // multi-line continuations. Only lines starting with space/tab extend the previous header, // preventing malformed headers from corrupting adjacent fields. func extractAllHeaders(rawMessage string) []MessageHeader { -rawHeaderMessage := rawMessage -if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd >= 0 { - rawHeaderMessage = rawMessage[:headerEnd] -} else if headerEnd := strings.Index(rawMessage, "\n\n"); headerEnd >= 0 { - rawHeaderMessage = rawMessage[:headerEnd] -} + rawHeaderMessage := rawMessage + if headerEnd := strings.Index(rawMessage, "\r\n\r\n"); headerEnd >= 0 { + rawHeaderMessage = rawMessage[:headerEnd] + } else if headerEnd := strings.Index(rawMessage, "\n\n"); headerEnd >= 0 { + rawHeaderMessage = rawMessage[:headerEnd] + } var headers []MessageHeader lines := strings.Split(rawHeaderMessage, "\n") sequence := 0 From 5d66c9e7f1d226584893fa4a7c515d1959418deb Mon Sep 17 00:00:00 2001 From: prdai Date: Thu, 11 Jun 2026 06:41:08 +0530 Subject: [PATCH 4/4] fix: preserve final header after header-section truncation The header-section-only refactor truncates the raw message at the header/body boundary, which removes the trailing blank line the parse loop relied on to flush the last header. As a result the final header was silently dropped. Flush the pending header after the loop instead. Add tests covering body exclusion for both \r\n\r\n and \n\n separators, and a regression test asserting the first-blank-line break still stops header parsing for mixed line endings (header-injection guard). --- internal/delivery/parser/parser.go | 16 +++--- internal/delivery/parser/parser_test.go | 71 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/internal/delivery/parser/parser.go b/internal/delivery/parser/parser.go index 64d69bc..6e8796c 100644 --- a/internal/delivery/parser/parser.go +++ b/internal/delivery/parser/parser.go @@ -1198,14 +1198,6 @@ func extractAllHeaders(rawMessage string) []MessageHeader { // Empty line marks end of headers section if line == "" { - // Save last header if exists - if currentHeaderName != "" { - headers = append(headers, MessageHeader{ - Name: currentHeaderName, - Value: currentHeaderValue.String(), - Sequence: sequence, - }) - } break } @@ -1243,5 +1235,13 @@ func extractAllHeaders(rawMessage string) []MessageHeader { currentHeaderValue.WriteString(strings.TrimSpace(line[colonIdx+1:])) } + if currentHeaderName != "" { + headers = append(headers, MessageHeader{ + Name: currentHeaderName, + Value: currentHeaderValue.String(), + Sequence: sequence, + }) + } + return headers } diff --git a/internal/delivery/parser/parser_test.go b/internal/delivery/parser/parser_test.go index 71e5923..94a7f9f 100644 --- a/internal/delivery/parser/parser_test.go +++ b/internal/delivery/parser/parser_test.go @@ -589,6 +589,77 @@ Body starts here.` } } +func TestExtractAllHeadersExcludesBody(t *testing.T) { + tests := []struct { + name string + rawMessage string + }{ + { + name: "CRLF separator", + rawMessage: "From: sender@example.com\r\n" + + "To: recipient@example.com\r\n" + + "Subject: Test Subject\r\n" + + "\r\n" + + "This is the body.\r\n" + + "X-Injected: evil\r\n" + + "More body text.\r\n", + }, + { + name: "LF separator", + rawMessage: "From: sender@example.com\n" + + "To: recipient@example.com\n" + + "Subject: Test Subject\n" + + "\n" + + "This is the body.\n" + + "X-Injected: evil\n" + + "More body text.\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg, err := parser.ParseMIMEMessage(tt.rawMessage) + if err != nil { + t.Fatalf("Failed to parse message: %v", err) + } + + foundSubject := false + for _, header := range msg.Headers { + if header.Name == "X-Injected" { + t.Errorf("Expected no X-Injected header from body, got %q", header.Value) + } + if header.Name == "Subject" { + foundSubject = true + } + } + + if !foundSubject { + t.Error("Expected Subject header to be extracted from the header section") + } + }) + } +} + +func TestExtractAllHeadersStopsAtFirstBlankLine(t *testing.T) { + rawMessage := "From: sender@example.com\r\n" + + "To: recipient@example.com\n" + + "\n" + + "X-Injected: evil\r\n" + + "\r\n" + + "Body text.\r\n" + + msg, err := parser.ParseMIMEMessage(rawMessage) + if err != nil { + t.Fatalf("Failed to parse message: %v", err) + } + + for _, header := range msg.Headers { + if header.Name == "X-Injected" { + t.Errorf("Expected no X-Injected header from body, got %q", header.Value) + } + } +} + func TestIsValidEmail(t *testing.T) { tests := []struct { name string