Every message relayed through usesend/smtp-proxy whose To header carries a display name is rejected by useSend own public API, so it never leaves the proxy.
Cause
apps/smtp-server/src/server.ts (current main, bd83535) forwards the address headers in their raw RFC 5322 form:
to: Array.isArray(parsed.to)
? parsed.to.map((addr) => addr.text).join(", ")
: parsed.to?.text,
parsed.to.text is "Gabriel Ferreira" <gabriel@example.com>, but apps/web/src/server/public-api/schemas/email-schema.ts validates recipients as bare addresses:
const emailAddress = z.string().email().max(320);
const recipients = emailAddress.or(z.array(emailAddress).min(1).max(50));
// to: recipients
from is unaffected because its schema is z.string().min(3).max(512), free-form. That asymmetry is why the error names only to.
The .join(", ") branch is broken for the same reason: a comma-joined string is not a single valid address either, so two plain recipients also fail. Same applies to cc/bcc on main.
Reproduction
Point any SMTP client that sets a display name (Documenso, Plane CE, Penpot, most mail clients) at the proxy and send one message. Proxy log:
Sending email to useSend API at: http://usesend:3000/api/v1/emails
useSend API error response: error: "{\"success\":false,\"error\":{\"issues\":[{\"validation\":\"email\",\"code\":\"invalid_string\",\"message\":\"Invalid email\",\"path\":[\"to\"]}]}}"
email data: {"to":"\"Gabriel da Silva Ferreira\" <gabriel@quartzo.ai>","from":"\"Quartzo Docs\" <noreply@quartzo.ai>", ...}
The SMTP transaction then fails, so the sending app sees a delivery error and no Email row is ever created.
Suggested fix
Use mailparser .value (the parsed { name, address } list) instead of .text, and send an array — the schema already accepts z.array(emailAddress):
function toAddressList(field): string[] | undefined {
if (!field) return undefined;
const groups = Array.isArray(field) ? field : [field];
const addresses = groups
.flatMap((g) => g?.value ?? [])
.map((e) => e?.address)
.filter(Boolean);
return addresses.length > 0 ? addresses : undefined;
}
Applied to to, cc, bcc and replyTo; from should keep .text so the display name survives.
Happy to send a PR if that shape looks right. We are running this patch in a fork today and mail flows.
Every message relayed through
usesend/smtp-proxywhoseToheader carries a display name is rejected by useSend own public API, so it never leaves the proxy.Cause
apps/smtp-server/src/server.ts(currentmain, bd83535) forwards the address headers in their raw RFC 5322 form:parsed.to.textis"Gabriel Ferreira" <gabriel@example.com>, butapps/web/src/server/public-api/schemas/email-schema.tsvalidates recipients as bare addresses:fromis unaffected because its schema isz.string().min(3).max(512), free-form. That asymmetry is why the error names onlyto.The
.join(", ")branch is broken for the same reason: a comma-joined string is not a single valid address either, so two plain recipients also fail. Same applies tocc/bcconmain.Reproduction
Point any SMTP client that sets a display name (Documenso, Plane CE, Penpot, most mail clients) at the proxy and send one message. Proxy log:
The SMTP transaction then fails, so the sending app sees a delivery error and no
Emailrow is ever created.Suggested fix
Use mailparser
.value(the parsed{ name, address }list) instead of.text, and send an array — the schema already acceptsz.array(emailAddress):Applied to
to,cc,bccandreplyTo;fromshould keep.textso the display name survives.Happy to send a PR if that shape looks right. We are running this patch in a fork today and mail flows.