fix(email): read the MailChannels sender through the address helper

@types/nodemailer 8 types a message's `from` as one address or a list, as
nodemailer has always accepted. The transport only knew a string or one
Address, so the list case did not type-check and, at run time, posted a
sender with no email.

The sender now goes through toMailChannelsAddresses, which already handles
all three shapes for to, cc and bcc, and the first address is the sender. A
string or a single Address comes out exactly as before.
mailchannels.test.ts sends each shape through nodemailer and reads what was
posted; against the old code the list case fails with `{}`.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-19 04:27:20 -07:00
co-authored by Hanzo Dev
parent 715cf40886
commit f29acbe6ae
2 changed files with 56 additions and 7 deletions
@@ -0,0 +1,54 @@
import assert from 'node:assert';
import { afterEach, describe, it } from 'node:test';
import { createTransport } from 'nodemailer';
import type { Options } from 'nodemailer/lib/mailer';
import { MailChannelsTransport } from '../transports/mailchannels';
const realFetch = globalThis.fetch;
/** Sends one mail through nodemailer and returns the `from` MailChannels was posted. */
const sendFrom = async (from: Options['from']) => {
const bodies: string[] = [];
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
bodies.push(String(init?.body));
return Promise.resolve(new Response('{}', { status: 202 }));
};
await createTransport(MailChannelsTransport.makeTransport({ apiKey: 'key' })).sendMail({
from,
to: '[email protected]',
subject: 'hi',
html: '<p>hi</p>',
});
assert.equal(bodies.length, 1);
return JSON.parse(bodies[0]).from;
};
describe('mailchannels transport', () => {
afterEach(() => {
globalThis.fetch = realFetch;
});
it('sends from a bare address', async () => {
assert.deepEqual(await sendFrom('[email protected]'), { email: '[email protected]' });
});
it('sends from a named address', async () => {
assert.deepEqual(await sendFrom({ name: 'Sign', address: '[email protected]' }), {
email: '[email protected]',
name: 'Sign',
});
});
it('sends from the first address of a list', async () => {
assert.deepEqual(await sendFrom(['[email protected]', '[email protected]']), {
email: '[email protected]',
});
});
});
+2 -7
View File
@@ -54,13 +54,8 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
const mailCc = this.toMailChannelsAddresses(mail.data.cc);
const mailBcc = this.toMailChannelsAddresses(mail.data.bcc);
const from: MailChannelsAddress =
typeof mail.data.from === 'string'
? { email: mail.data.from }
: {
email: mail.data.from?.address,
name: mail.data.from?.name,
};
// nodemailer takes a list here as well as one address; a message has one sender.
const [from] = this.toMailChannelsAddresses(mail.data.from);
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',