Files

167 lines
6.4 KiB
JavaScript
Raw Permalink Normal View History

2026-09-15 13:00:32 +02:00
import Mail from './mailer/index.js';
import * as shared from './shared/index.js';
import SMTPPool from './smtp-pool/index.js';
import SMTPTransport from './smtp-transport/index.js';
import SendmailTransport from './sendmail-transport/index.js';
import StreamTransport from './stream-transport/index.js';
import JSONTransport from './json-transport/index.js';
import SESTransport from './ses-transport/index.js';
import * as errors from './errors.js';
import nmfetch from './fetch/index.js';
import * as packageData from './package-info.js';
2020-02-29 23:01:03 +01:00
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
2025-06-13 19:44:16 +02:00
const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
2020-11-12 16:31:20 +01:00
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
2020-02-29 23:01:03 +01:00
let testAccount = false;
2026-09-15 13:00:32 +02:00
export function createTransport(transporter, defaults) {
2020-02-29 23:01:03 +01:00
let options;
if (
2026-09-15 13:00:32 +02:00
// provided transporter is a configuration object, not transporter plugin
(typeof transporter === 'object' && typeof transporter.send !== 'function') ||
2020-02-29 23:01:03 +01:00
// provided transporter looks like a connection url
2026-09-15 13:00:32 +02:00
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))) {
2026-04-28 12:50:45 +02:00
const urlConfig = typeof transporter === 'string' ? transporter : transporter.url;
if (urlConfig) {
2026-09-15 13:00:32 +02:00
// parse a configuration URL into configuration options. The other keys of a
// configuration object apply where the url does not set them, merged the same
// way the SMTP transports merge their own url option
const parsed = shared.parseConnectionUrl(urlConfig);
options = (typeof transporter === 'object'
? shared.assign(false, shared.copyOwnKeys({}, transporter, key => key === 'url'), parsed)
: parsed);
}
else {
2020-02-29 23:01:03 +01:00
options = transporter;
}
if (options.pool) {
transporter = new SMTPPool(options);
2026-09-15 13:00:32 +02:00
}
else if (options.sendmail) {
2020-02-29 23:01:03 +01:00
transporter = new SendmailTransport(options);
2026-09-15 13:00:32 +02:00
}
else if (options.streamTransport) {
2020-02-29 23:01:03 +01:00
transporter = new StreamTransport(options);
2026-09-15 13:00:32 +02:00
}
else if (options.jsonTransport) {
2020-02-29 23:01:03 +01:00
transporter = new JSONTransport(options);
2026-09-15 13:00:32 +02:00
}
else if (options.SES) {
const ses = options.SES;
if (ses.ses && ses.aws) {
const error = new Error('Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
2026-02-05 08:49:11 +01:00
error.code = errors.ECONFIG;
2025-06-14 22:45:43 +02:00
throw error;
}
2020-02-29 23:01:03 +01:00
transporter = new SESTransport(options);
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
transporter = new SMTPTransport(options);
}
}
2026-09-15 13:00:32 +02:00
return new Mail(transporter, options, defaults);
}
export function createTestAccount(apiUrl, callback) {
2020-02-29 23:01:03 +01:00
let promise;
if (!callback && typeof apiUrl === 'function') {
callback = apiUrl;
apiUrl = false;
}
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
2026-09-15 13:00:32 +02:00
const done = callback;
2020-02-29 23:01:03 +01:00
if (ETHEREAL_CACHE && testAccount) {
2026-09-15 13:00:32 +02:00
setImmediate(() => done(null, testAccount));
2020-02-29 23:01:03 +01:00
return promise;
}
apiUrl = apiUrl || ETHEREAL_API;
2026-04-28 12:50:45 +02:00
const chunks = [];
2020-02-29 23:01:03 +01:00
let chunklen = 0;
2026-04-28 12:50:45 +02:00
const requestHeaders = {};
const requestBody = {
2025-06-13 19:44:16 +02:00
requestor: packageData.name,
version: packageData.version
};
if (ETHEREAL_API_KEY) {
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
}
2026-06-15 07:32:52 +02:00
const fetchOptions = {
2020-02-29 23:01:03 +01:00
contentType: 'application/json',
method: 'POST',
2025-06-13 19:44:16 +02:00
headers: requestHeaders,
body: Buffer.from(JSON.stringify(requestBody))
2026-06-15 07:32:52 +02:00
};
2026-09-15 13:00:32 +02:00
// Credential-bearing request to the Ethereal API. src/fetch already
2026-06-15 07:32:52 +02:00
// validates certs by default; pin rejectUnauthorized:true here so this
// call stays strict regardless of any future default change and is never
// relaxed for a real-cert endpoint.
if (/^https:/i.test(apiUrl)) {
fetchOptions.tls = { rejectUnauthorized: true };
}
const req = nmfetch(apiUrl + '/user', fetchOptions);
2020-02-29 23:01:03 +01:00
req.on('readable', () => {
let chunk;
while ((chunk = req.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
2026-09-15 13:00:32 +02:00
req.once('error', err => done(err));
2020-02-29 23:01:03 +01:00
req.once('end', () => {
2026-04-28 12:50:45 +02:00
const res = Buffer.concat(chunks, chunklen);
2020-02-29 23:01:03 +01:00
let data;
try {
data = JSON.parse(res.toString());
2026-09-15 13:00:32 +02:00
}
catch (E) {
return done(E);
2020-02-29 23:01:03 +01:00
}
if (data.status !== 'success' || data.error) {
2026-09-15 13:00:32 +02:00
return done(new Error(data.error || 'Request failed'));
2020-02-29 23:01:03 +01:00
}
delete data.status;
testAccount = data;
2026-09-15 13:00:32 +02:00
done(null, testAccount);
2020-02-29 23:01:03 +01:00
});
return promise;
2026-09-15 13:00:32 +02:00
}
/**
* Resolves the Ethereal web URL for a message sent through an Ethereal test account
*
* @param info Result object of sendMail()
* @returns URL of the message in the Ethereal web interface, or false if the response does not carry one
*/
export function getTestMessageUrl(info) {
2020-02-29 23:01:03 +01:00
if (!info || !info.response) {
return false;
}
2026-04-28 12:50:45 +02:00
const infoProps = new Map();
2026-06-15 07:32:52 +02:00
// Extract the trailing "[...]" part of the response (no "]" allowed inside)
// with linear string scanning; the equivalent regex /\[([^\]]+)\]$/ was
// flagged for polynomial backtracking on adversarial server responses
const response = info.response.toString();
if (response.length > 2 && response.charAt(response.length - 1) === ']') {
const open = response.indexOf('[', response.lastIndexOf(']', response.length - 2) + 1);
if (open >= 0 && open < response.length - 2) {
const props = response.substring(open + 1, response.length - 1);
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
infoProps.set(key, value);
2026-09-15 13:00:32 +02:00
return m;
2026-06-15 07:32:52 +02:00
});
}
}
2020-02-29 23:01:03 +01:00
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
2026-09-15 13:00:32 +02:00
return ((testAccount && testAccount.web) || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
2020-02-29 23:01:03 +01:00
}
return false;
2026-09-15 13:00:32 +02:00
}
const nodemailer = {
createTransport,
createTestAccount,
getTestMessageUrl
2020-02-29 23:01:03 +01:00
};
2026-09-15 13:00:32 +02:00
export default nodemailer;