Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/smtp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ const SSL_KEY_PATH =
const SSL_CERT_PATH =
process.env.USESEND_API_CERT_PATH ?? process.env.UNSEND_API_CERT_PATH;

// Forwarded headers that mailparser's normalized Map can't be trusted for.
// Read from headerLines (raw) instead.
const FORWARDED_HEADERS = ["list-unsubscribe", "list-unsubscribe-post"];

function canonicalHeaderName(name: string): string {
return name
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("-");
}

function extractForwardedHeaders(
headerLines: readonly { key: string; line: string }[] | undefined,
): Record<string, string> | undefined {
const result: Record<string, string> = {};

for (const { key, line } of headerLines || []) {
if (!FORWARDED_HEADERS.includes(key)) {
continue;
}
const colonIndex = line.indexOf(":");
if (colonIndex === -1) {
continue;
}
const value = line.slice(colonIndex + 1).trim();
if (value.length > 0) {
result[canonicalHeaderName(key)] = value;
Comment on lines +43 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== server.ts outline ==\n'
ast-grep outline apps/smtp-server/src/server.ts --view expanded || true

printf '\n== searches ==\n'
rg -n "canonicalHeaderName|headerLines|mailparser|List-Unsubscribe|unfold|CRLF|replace\\(/\\\\r\\?\\\\n" apps/smtp-server/src -S || true

printf '\n== targeted snippet ==\n'
sed -n '1,140p' apps/smtp-server/src/server.ts

Repository: usesend/useSend

Length of output: 5972


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package refs ==\n'
rg -n '"mailparser"|simpleParser|headerLines' package.json pnpm-lock.yaml package-lock.json yarn.lock apps -S || true

printf '\n== test files under smtp-server ==\n'
fd -t f '.*(test|spec)\.(ts|tsx|js|jsx)$' apps/smtp-server . || true

printf '\n== smtp-server tree ==\n'
fd -t f apps/smtp-server || true

Repository: usesend/useSend

Length of output: 3154


🌐 Web query:

mailparser simpleParser headerLines folded header values line property raw headers unfolding RFC 5322

💡 Result:

In the context of the mailparser library, the headerLines property and RFC 5322 specifications relate to how raw email headers are handled, specifically regarding folding and unfolding [1][2]. RFC 5322 defines header folding as the process of splitting a long header field body into multiple lines by inserting a CRLF (Carriage Return + Line Feed) followed by WSP (white space) [2]. Conversely, unfolding is the process of reversing this to reconstruct the original, single-line header field by removing the CRLF and subsequent white space [2][3]. This ensures that headers can be evaluated without length restrictions [2]. In mailparser, the headerLines event provides access to these raw header segments [1]. When you listen for this event, the library emits an array of objects, where each object contains two properties [1]: - key: The lowercase header name [1]. - line: The raw content of that header line as it appears in the message, including any folding [1]. While mailparser processes these raw lines into a structured Map of decoded header values for easier use (e.g., parsing dates, email addresses, or list headers), the headerLines event exposes the unparsed, potentially folded lines for developers who need to access the raw data exactly as it was received [1]. Top results: [1][2][4]

Citations:


🌐 Web query:

site:github.com nodemailer/mailparser headerLines line folded header raw as it appears in the message

💡 Result:

In nodemailer/mailparser, the headerLines property is an array of objects that provides access to the header information, where each object contains the key and the line (the raw, folded header string as it appeared in the original message) [1][2]. When parsing an email, mailparser stores individual header lines in this array [2]. Because the library performs parsing and transformations on headers, headerLines serves as a reliable way to access the original, verbatim header lines, including their folding (multi-line) format [1][2]. If you are using the MailParser class directly, you can access headerLines after the parsing process has begun or finished [1]. As of recent updates, you can also listen for the headerLines event to gain access to these raw lines during the parsing stream [1]. Example usage: const { simpleParser } = require('mailparser'); simpleParser(source).then(parsed => { // Access the raw folded lines via parsed.headerLines parsed.headerLines.forEach(header => { console.log(Key: ${header.key}); console.log(Raw Line: ${header.line}); }); }); Note that mailparser is designed to be a streaming parser [3], and while it provides structured header objects (which are often Maps), the headerLines array is the standard mechanism provided by the library to retrieve the exact, raw, folded header content for further analysis or manual reconstruction [1][2].

Citations:


Unfold folded List-Unsubscribe header values before forwarding. headerLines preserves raw folding, so .trim() leaves embedded CRLF + WSP intact. Unfold the value before storing it; otherwise valid unsubscribe headers can be forwarded malformed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/smtp-server/src/server.ts` around lines 43 - 45, Update the header
parsing flow around canonicalHeaderName to unfold folded header values before
storing them in result: replace embedded CRLF followed by whitespace with a
single space, then trim the normalized value and preserve the existing non-empty
check and forwarding behavior.

}
}

return Object.keys(result).length > 0 ? result : undefined;
}

async function sendEmailToUseSend(emailData: any, apiKey: string) {
try {
const apiEndpoint = "/api/v1/emails";
Expand Down Expand Up @@ -88,6 +121,8 @@ const serverOptions: SMTPServerOptions = {
return callback(new Error("No API key found in session"));
}

const forwardedHeaders = extractForwardedHeaders(parsed.headerLines);

const emailObject = {
to: Array.isArray(parsed.to)
? parsed.to.map((addr) => addr.text).join(", ")
Expand All @@ -99,6 +134,7 @@ const serverOptions: SMTPServerOptions = {
text: parsed.text,
html: parsed.html,
replyTo: parsed.replyTo?.text,
headers: forwardedHeaders,
attachments:
parsed.attachments.length > 0
? parsed.attachments.map((attachment, index) => ({
Expand Down