sently 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +367 -0
- package/dist/chunk-794hc3m4.js +105 -0
- package/dist/chunk-794hc3m4.js.map +10 -0
- package/dist/chunk-hdqpvsm8.js +116 -0
- package/dist/chunk-hdqpvsm8.js.map +11 -0
- package/dist/chunk-tch6s785.js +943 -0
- package/dist/chunk-tch6s785.js.map +14 -0
- package/dist/chunk-v0bahtg2.js +6 -0
- package/dist/chunk-v0bahtg2.js.map +9 -0
- package/dist/chunk-vm70w4e3.js +68 -0
- package/dist/chunk-vm70w4e3.js.map +10 -0
- package/dist/src/adapters/bun.js +175 -0
- package/dist/src/adapters/bun.js.map +10 -0
- package/dist/src/adapters/cf.js +78 -0
- package/dist/src/adapters/cf.js.map +10 -0
- package/dist/src/adapters/deno.js +73 -0
- package/dist/src/adapters/deno.js.map +10 -0
- package/dist/src/adapters/node.js +172 -0
- package/dist/src/adapters/node.js.map +10 -0
- package/dist/src/auth/oauth2.js +14 -0
- package/dist/src/auth/oauth2.js.map +9 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +9 -0
- package/dist/src/pool/pool.js +263 -0
- package/dist/src/pool/pool.js.map +11 -0
- package/dist/src/transports/postmark.js +85 -0
- package/dist/src/transports/postmark.js.map +10 -0
- package/dist/src/transports/resend.js +86 -0
- package/dist/src/transports/resend.js.map +10 -0
- package/dist/src/transports/sendgrid.js +104 -0
- package/dist/src/transports/sendgrid.js.map +10 -0
- package/dist/src/transports/smtp.js +24 -0
- package/dist/src/transports/smtp.js.map +9 -0
- package/package.json +105 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OAuth2Client
|
|
3
|
+
} from "./chunk-vm70w4e3.js";
|
|
4
|
+
import {
|
|
5
|
+
extractEmails,
|
|
6
|
+
parseAddresses,
|
|
7
|
+
resolveAttachments,
|
|
8
|
+
toMIMEHeader
|
|
9
|
+
} from "./chunk-hdqpvsm8.js";
|
|
10
|
+
import {
|
|
11
|
+
decodeBase64,
|
|
12
|
+
encodeBase64,
|
|
13
|
+
encodeHeader,
|
|
14
|
+
encodeUtf8
|
|
15
|
+
} from "./chunk-794hc3m4.js";
|
|
16
|
+
import {
|
|
17
|
+
__require
|
|
18
|
+
} from "./chunk-v0bahtg2.js";
|
|
19
|
+
|
|
20
|
+
// src/core/dkim.ts
|
|
21
|
+
var CRLF = `\r
|
|
22
|
+
`;
|
|
23
|
+
var DEFAULT_HEADER_FIELDS = "from:to:subject:date:message-id:mime-version:content-type";
|
|
24
|
+
function canonicalizeHeadersRelaxed(headers, fieldNames) {
|
|
25
|
+
const parsed = parseHeaders(headers);
|
|
26
|
+
const lines = [];
|
|
27
|
+
for (const name of fieldNames) {
|
|
28
|
+
const key = name.toLowerCase().trim();
|
|
29
|
+
const values = parsed.get(key);
|
|
30
|
+
if (!values) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const value of values) {
|
|
34
|
+
const unfolded = value.replace(/\r?\n/g, "").replace(/\s+/g, " ").trim();
|
|
35
|
+
lines.push(`${key}:${unfolded}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.length > 0 ? `${lines.join(CRLF)}${CRLF}` : "";
|
|
39
|
+
}
|
|
40
|
+
function canonicalizeBodyRelaxed(body) {
|
|
41
|
+
const normalized = body.replace(/\r\n/g, `
|
|
42
|
+
`).replace(/\r/g, `
|
|
43
|
+
`);
|
|
44
|
+
const lines = normalized.split(`
|
|
45
|
+
`).map((line) => line.replace(/[ \t]+$/g, "").replace(/[ \t]+/g, " ")).filter((line) => line.length > 0);
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
return CRLF;
|
|
48
|
+
}
|
|
49
|
+
return `${lines.join(CRLF)}${CRLF}`;
|
|
50
|
+
}
|
|
51
|
+
async function importPrivateKey(pem, algorithm) {
|
|
52
|
+
if (algorithm === "ed25519-sha256" && /OPENSSH PRIVATE KEY/i.test(pem)) {
|
|
53
|
+
throw new Error("Ed25519 keys must be in PKCS#8 PEM format (-----BEGIN PRIVATE KEY-----). Convert with: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key_pkcs8.pem");
|
|
54
|
+
}
|
|
55
|
+
const der = pemToDer(pem);
|
|
56
|
+
const derBuffer = toArrayBuffer(der);
|
|
57
|
+
try {
|
|
58
|
+
if (algorithm === "ed25519-sha256") {
|
|
59
|
+
return await crypto.subtle.importKey("pkcs8", derBuffer, { name: "Ed25519" }, false, [
|
|
60
|
+
"sign"
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
return await crypto.subtle.importKey("pkcs8", derBuffer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (algorithm === "ed25519-sha256" && err instanceof DOMException) {
|
|
66
|
+
throw new Error("Ed25519 DKIM requires Node.js ≥ 18.4, Bun ≥ 1.0, or Cloudflare Workers", {
|
|
67
|
+
cause: err
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function signDKIM(rawMessage, config) {
|
|
74
|
+
const algorithm = config.algorithm ?? "rsa-sha256";
|
|
75
|
+
const fieldList = (config.headerFieldNames ?? DEFAULT_HEADER_FIELDS).split(":").map((f) => f.trim().toLowerCase()).filter(Boolean);
|
|
76
|
+
const skip = new Set((config.skipFields ?? "").split(":").map((f) => f.trim().toLowerCase()).filter(Boolean));
|
|
77
|
+
const signFields = fieldList.filter((f) => !skip.has(f));
|
|
78
|
+
const text = new TextDecoder().decode(rawMessage);
|
|
79
|
+
const sep = text.indexOf(`\r
|
|
80
|
+
\r
|
|
81
|
+
`);
|
|
82
|
+
const headerPart = sep >= 0 ? text.slice(0, sep) : text;
|
|
83
|
+
const bodyPart = sep >= 0 ? text.slice(sep + 4) : "";
|
|
84
|
+
const bodyCanonical = canonicalizeBodyRelaxed(bodyPart);
|
|
85
|
+
const bodyHash = await sha256Base64(encodeUtf8(bodyCanonical));
|
|
86
|
+
const dkimAlgo = algorithm === "ed25519-sha256" ? "ed25519-sha256" : "rsa-sha256";
|
|
87
|
+
const headerList = signFields.join(":");
|
|
88
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
89
|
+
const dkimWithoutSig = [
|
|
90
|
+
`v=1`,
|
|
91
|
+
`a=${dkimAlgo}`,
|
|
92
|
+
`c=relaxed/relaxed`,
|
|
93
|
+
`d=${config.domainName}`,
|
|
94
|
+
`s=${config.keySelector}`,
|
|
95
|
+
`h=${headerList}`,
|
|
96
|
+
`bh=${bodyHash}`,
|
|
97
|
+
`b=`,
|
|
98
|
+
`t=${timestamp}`
|
|
99
|
+
].join("; ");
|
|
100
|
+
const dkimHeaderName = "dkim-signature";
|
|
101
|
+
const dkimHeaderValue = dkimWithoutSig;
|
|
102
|
+
const headersWithDkim = `${headerPart}${CRLF}${dkimHeaderName}:${dkimHeaderValue}`;
|
|
103
|
+
const canonical = canonicalizeHeadersRelaxed(headersWithDkim, [...signFields, dkimHeaderName]);
|
|
104
|
+
const key = await importPrivateKey(config.privateKey, algorithm);
|
|
105
|
+
const data = encodeUtf8(canonical);
|
|
106
|
+
const signature = await signData(key, data, algorithm);
|
|
107
|
+
const bValue = encodeBase64(signature).replace(/\r\n/g, "");
|
|
108
|
+
const header = `DKIM-Signature: v=1; a=${dkimAlgo}; c=relaxed/relaxed; d=${config.domainName}; s=${config.keySelector}; h=${headerList}; bh=${bodyHash}; b=${bValue}; t=${timestamp}`;
|
|
109
|
+
return { header };
|
|
110
|
+
}
|
|
111
|
+
function parseHeaders(headerBlock) {
|
|
112
|
+
const map = new Map;
|
|
113
|
+
const lines = headerBlock.split(/\r?\n/).filter((l) => l.length > 0);
|
|
114
|
+
let currentName = "";
|
|
115
|
+
let currentValue = "";
|
|
116
|
+
const flush = () => {
|
|
117
|
+
if (!currentName) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const key = currentName.toLowerCase();
|
|
121
|
+
const list = map.get(key) ?? [];
|
|
122
|
+
list.push(currentValue);
|
|
123
|
+
map.set(key, list);
|
|
124
|
+
currentName = "";
|
|
125
|
+
currentValue = "";
|
|
126
|
+
};
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (/^[ \t]/.test(line) && currentName) {
|
|
129
|
+
currentValue += ` ${line.trim()}`;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
flush();
|
|
133
|
+
const colon = line.indexOf(":");
|
|
134
|
+
if (colon > 0) {
|
|
135
|
+
currentName = line.slice(0, colon).trim();
|
|
136
|
+
currentValue = line.slice(colon + 1).trim();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
flush();
|
|
140
|
+
return map;
|
|
141
|
+
}
|
|
142
|
+
function pemToDer(pem) {
|
|
143
|
+
const lines = pem.replace(/-----BEGIN[^-]+-----/g, "").replace(/-----END[^-]+-----/g, "").replace(/\s/g, "");
|
|
144
|
+
const binary = atob(lines);
|
|
145
|
+
const der = new Uint8Array(binary.length);
|
|
146
|
+
for (let i = 0;i < binary.length; i++) {
|
|
147
|
+
der[i] = binary.charCodeAt(i);
|
|
148
|
+
}
|
|
149
|
+
return der;
|
|
150
|
+
}
|
|
151
|
+
function toArrayBuffer(bytes) {
|
|
152
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
153
|
+
}
|
|
154
|
+
async function sha256Base64(data) {
|
|
155
|
+
const hash = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
156
|
+
return encodeBase64(new Uint8Array(hash)).replace(/\r\n/g, "");
|
|
157
|
+
}
|
|
158
|
+
async function signData(key, data, algorithm) {
|
|
159
|
+
const buf = toArrayBuffer(data);
|
|
160
|
+
if (algorithm === "ed25519-sha256") {
|
|
161
|
+
const sig2 = await crypto.subtle.sign("Ed25519", key, buf);
|
|
162
|
+
return new Uint8Array(sig2);
|
|
163
|
+
}
|
|
164
|
+
const sig = await crypto.subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, key, buf);
|
|
165
|
+
return new Uint8Array(sig);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/core/mime.ts
|
|
169
|
+
var CRLF2 = `\r
|
|
170
|
+
`;
|
|
171
|
+
async function buildMIME(options, dkim) {
|
|
172
|
+
const messageId = options.messageId ?? generateMessageId();
|
|
173
|
+
const date = (options.date ?? new Date).toUTCString();
|
|
174
|
+
const fromAddrs = parseAddresses(options.from);
|
|
175
|
+
const toAddrs = parseAddresses(options.to);
|
|
176
|
+
const ccAddrs = options.cc ? parseAddresses(options.cc) : [];
|
|
177
|
+
if (fromAddrs.length === 0) {
|
|
178
|
+
throw new Error("Missing from address");
|
|
179
|
+
}
|
|
180
|
+
if (toAddrs.length === 0) {
|
|
181
|
+
throw new Error("Missing to address");
|
|
182
|
+
}
|
|
183
|
+
const envelope = {
|
|
184
|
+
from: fromAddrs[0]?.address ?? "",
|
|
185
|
+
to: [
|
|
186
|
+
...extractEmails(options.to),
|
|
187
|
+
...options.cc ? extractEmails(options.cc) : [],
|
|
188
|
+
...options.bcc ? extractEmails(options.bcc) : []
|
|
189
|
+
]
|
|
190
|
+
};
|
|
191
|
+
const attachments = options.attachments ?? [];
|
|
192
|
+
const inlineAttachments = attachments.filter((a) => a.inline || a.contentId);
|
|
193
|
+
const regularAttachments = attachments.filter((a) => !a.inline && !a.contentId);
|
|
194
|
+
let root = buildSimpleBody(options);
|
|
195
|
+
if (inlineAttachments.length > 0) {
|
|
196
|
+
const boundary = generateBoundary();
|
|
197
|
+
root = {
|
|
198
|
+
contentType: `multipart/related; boundary="${boundary}"`,
|
|
199
|
+
content: assembleMultipart(boundary, [
|
|
200
|
+
formatNestedPart(buildSimpleBody(options)),
|
|
201
|
+
...inlineAttachments.map(formatAttachmentPart)
|
|
202
|
+
])
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (regularAttachments.length > 0) {
|
|
206
|
+
const boundary = generateBoundary();
|
|
207
|
+
root = {
|
|
208
|
+
contentType: `multipart/mixed; boundary="${boundary}"`,
|
|
209
|
+
content: assembleMultipart(boundary, [
|
|
210
|
+
formatNestedPart(root),
|
|
211
|
+
...regularAttachments.map(formatAttachmentPart)
|
|
212
|
+
])
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const headers = [
|
|
216
|
+
foldHeader("From", fromAddrs.map(toMIMEHeader).join(", ")),
|
|
217
|
+
foldHeader("To", toAddrs.map(toMIMEHeader).join(", "))
|
|
218
|
+
];
|
|
219
|
+
if (ccAddrs.length > 0) {
|
|
220
|
+
headers.push(foldHeader("Cc", ccAddrs.map(toMIMEHeader).join(", ")));
|
|
221
|
+
}
|
|
222
|
+
if (options.replyTo) {
|
|
223
|
+
headers.push(foldHeader("Reply-To", parseAddresses(options.replyTo).map(toMIMEHeader).join(", ")));
|
|
224
|
+
}
|
|
225
|
+
headers.push(foldHeader("Subject", encodeHeader(options.subject)), foldHeader("Date", date), foldHeader("Message-ID", messageId), "MIME-Version: 1.0");
|
|
226
|
+
if (options.priority === "high") {
|
|
227
|
+
headers.push("X-Priority: 1", "Importance: high");
|
|
228
|
+
} else if (options.priority === "low") {
|
|
229
|
+
headers.push("X-Priority: 5", "Importance: low");
|
|
230
|
+
}
|
|
231
|
+
if (options.headers) {
|
|
232
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
233
|
+
headers.push(foldHeader(key, value));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
headers.push(`Content-Type: ${root.contentType}`);
|
|
237
|
+
if (root.contentTransferEncoding) {
|
|
238
|
+
headers.push(`Content-Transfer-Encoding: ${root.contentTransferEncoding}`);
|
|
239
|
+
}
|
|
240
|
+
const rawText = `${headers.join(CRLF2)}${CRLF2}${CRLF2}${root.content}`;
|
|
241
|
+
let raw = encodeUtf8(rawText);
|
|
242
|
+
if (dkim) {
|
|
243
|
+
const { header } = await signDKIM(raw, dkim);
|
|
244
|
+
const signedText = `${header}${CRLF2}${rawText}`;
|
|
245
|
+
raw = encodeUtf8(signedText);
|
|
246
|
+
}
|
|
247
|
+
return { raw, envelope, messageId, size: raw.length };
|
|
248
|
+
}
|
|
249
|
+
function buildSimpleBody(options) {
|
|
250
|
+
const hasText = Boolean(options.text);
|
|
251
|
+
const hasHtml = Boolean(options.html);
|
|
252
|
+
if (hasText && hasHtml) {
|
|
253
|
+
const boundary = generateBoundary();
|
|
254
|
+
return {
|
|
255
|
+
contentType: `multipart/alternative; boundary="${boundary}"`,
|
|
256
|
+
content: assembleMultipart(boundary, [
|
|
257
|
+
formatSimplePart({
|
|
258
|
+
contentType: "text/plain; charset=utf-8",
|
|
259
|
+
contentTransferEncoding: "8bit",
|
|
260
|
+
content: options.text ?? ""
|
|
261
|
+
}),
|
|
262
|
+
formatSimplePart({
|
|
263
|
+
contentType: "text/html; charset=utf-8",
|
|
264
|
+
contentTransferEncoding: "8bit",
|
|
265
|
+
content: options.html ?? ""
|
|
266
|
+
})
|
|
267
|
+
])
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (hasHtml) {
|
|
271
|
+
return {
|
|
272
|
+
contentType: "text/html; charset=utf-8",
|
|
273
|
+
contentTransferEncoding: "8bit",
|
|
274
|
+
content: options.html ?? ""
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
contentType: "text/plain; charset=utf-8",
|
|
279
|
+
contentTransferEncoding: "8bit",
|
|
280
|
+
content: options.text ?? ""
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function formatSimplePart(part) {
|
|
284
|
+
const headers = [`Content-Type: ${part.contentType}`];
|
|
285
|
+
if (part.contentTransferEncoding) {
|
|
286
|
+
headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);
|
|
287
|
+
}
|
|
288
|
+
return `${headers.join(CRLF2)}${CRLF2}${CRLF2}${part.content}`;
|
|
289
|
+
}
|
|
290
|
+
function formatNestedPart(part) {
|
|
291
|
+
const headers = [`Content-Type: ${part.contentType}`];
|
|
292
|
+
if (part.contentTransferEncoding) {
|
|
293
|
+
headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);
|
|
294
|
+
}
|
|
295
|
+
return `${headers.join(CRLF2)}${CRLF2}${CRLF2}${part.content}`;
|
|
296
|
+
}
|
|
297
|
+
function formatAttachmentPart(attachment) {
|
|
298
|
+
if (!attachment.content || typeof attachment.content === "string") {
|
|
299
|
+
throw new Error(`Attachment "${attachment.filename}" requires Uint8Array content`);
|
|
300
|
+
}
|
|
301
|
+
const headers = [
|
|
302
|
+
`Content-Type: ${attachment.contentType ?? "application/octet-stream"}`,
|
|
303
|
+
"Content-Transfer-Encoding: base64",
|
|
304
|
+
`Content-Disposition: ${attachment.inline ? "inline" : "attachment"}; filename="${attachment.filename}"`
|
|
305
|
+
];
|
|
306
|
+
if (attachment.contentId) {
|
|
307
|
+
headers.push(`Content-ID: <${attachment.contentId}>`);
|
|
308
|
+
}
|
|
309
|
+
if (attachment.headers) {
|
|
310
|
+
for (const [key, value] of Object.entries(attachment.headers)) {
|
|
311
|
+
headers.push(`${key}: ${value}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return `${headers.join(CRLF2)}${CRLF2}${CRLF2}${encodeBase64(attachment.content)}`;
|
|
315
|
+
}
|
|
316
|
+
function assembleMultipart(boundary, parts) {
|
|
317
|
+
const segments = parts.map((part) => `--${boundary}${CRLF2}${part}`);
|
|
318
|
+
segments.push(`--${boundary}--`);
|
|
319
|
+
return segments.join(CRLF2);
|
|
320
|
+
}
|
|
321
|
+
function generateMessageId() {
|
|
322
|
+
const random = crypto.getRandomValues(new Uint8Array(8));
|
|
323
|
+
const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
324
|
+
return `<${Date.now()}.${hex}@sently>`;
|
|
325
|
+
}
|
|
326
|
+
function generateBoundary() {
|
|
327
|
+
const random = crypto.getRandomValues(new Uint8Array(12));
|
|
328
|
+
const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
329
|
+
return `----sently_${hex}`;
|
|
330
|
+
}
|
|
331
|
+
function foldHeader(name, value) {
|
|
332
|
+
const line = `${name}: ${value}`;
|
|
333
|
+
if (line.length <= 76) {
|
|
334
|
+
return line;
|
|
335
|
+
}
|
|
336
|
+
const chunks = [];
|
|
337
|
+
let remaining = line;
|
|
338
|
+
while (remaining.length > 76) {
|
|
339
|
+
let breakAt = remaining.lastIndexOf(" ", 76);
|
|
340
|
+
if (breakAt <= name.length + 1) {
|
|
341
|
+
breakAt = 76;
|
|
342
|
+
}
|
|
343
|
+
chunks.push(remaining.slice(0, breakAt));
|
|
344
|
+
remaining = ` ${remaining.slice(breakAt).trimStart()}`;
|
|
345
|
+
}
|
|
346
|
+
chunks.push(remaining);
|
|
347
|
+
return chunks.join(`${CRLF2} `);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/core/cram-md5.ts
|
|
351
|
+
var BLOCK_SIZE = 64;
|
|
352
|
+
function u32(x) {
|
|
353
|
+
return x >>> 0;
|
|
354
|
+
}
|
|
355
|
+
function md5(data) {
|
|
356
|
+
const padded = padMessage(data);
|
|
357
|
+
let a0 = 1732584193;
|
|
358
|
+
let b0 = 4023233417;
|
|
359
|
+
let c0 = 2562383102;
|
|
360
|
+
let d0 = 271733878;
|
|
361
|
+
for (let i = 0;i < padded.length; i += 64) {
|
|
362
|
+
const block = padded.subarray(i, i + 64);
|
|
363
|
+
const m = new Uint32Array(16);
|
|
364
|
+
for (let j = 0;j < 16; j++) {
|
|
365
|
+
const o = j * 4;
|
|
366
|
+
m[j] = u32((block[o] ?? 0) | (block[o + 1] ?? 0) << 8 | (block[o + 2] ?? 0) << 16 | (block[o + 3] ?? 0) << 24);
|
|
367
|
+
}
|
|
368
|
+
let a = a0;
|
|
369
|
+
let b = b0;
|
|
370
|
+
let c = c0;
|
|
371
|
+
let d = d0;
|
|
372
|
+
for (let k = 0;k < 64; k++) {
|
|
373
|
+
let f;
|
|
374
|
+
let g;
|
|
375
|
+
if (k < 16) {
|
|
376
|
+
f = u32(b & c | ~b & d);
|
|
377
|
+
g = k;
|
|
378
|
+
} else if (k < 32) {
|
|
379
|
+
f = u32(b & d | c & ~d);
|
|
380
|
+
g = u32((5 * k + 1) % 16);
|
|
381
|
+
} else if (k < 48) {
|
|
382
|
+
f = u32(b ^ c ^ d);
|
|
383
|
+
g = u32((3 * k + 5) % 16);
|
|
384
|
+
} else {
|
|
385
|
+
f = u32(c ^ (b | ~d));
|
|
386
|
+
g = u32(7 * k % 16);
|
|
387
|
+
}
|
|
388
|
+
const temp = d;
|
|
389
|
+
d = c;
|
|
390
|
+
c = b;
|
|
391
|
+
b = u32(b + leftRotate(u32(a + f + u32((K[k] ?? 0) + (m[g] ?? 0))), S[k] ?? 0));
|
|
392
|
+
a = temp;
|
|
393
|
+
}
|
|
394
|
+
a0 = u32(a0 + a);
|
|
395
|
+
b0 = u32(b0 + b);
|
|
396
|
+
c0 = u32(c0 + c);
|
|
397
|
+
d0 = u32(d0 + d);
|
|
398
|
+
}
|
|
399
|
+
const out = new Uint8Array(16);
|
|
400
|
+
const view = new DataView(out.buffer);
|
|
401
|
+
view.setUint32(0, a0, true);
|
|
402
|
+
view.setUint32(4, b0, true);
|
|
403
|
+
view.setUint32(8, c0, true);
|
|
404
|
+
view.setUint32(12, d0, true);
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
var S = [
|
|
408
|
+
7,
|
|
409
|
+
12,
|
|
410
|
+
17,
|
|
411
|
+
22,
|
|
412
|
+
7,
|
|
413
|
+
12,
|
|
414
|
+
17,
|
|
415
|
+
22,
|
|
416
|
+
7,
|
|
417
|
+
12,
|
|
418
|
+
17,
|
|
419
|
+
22,
|
|
420
|
+
7,
|
|
421
|
+
12,
|
|
422
|
+
17,
|
|
423
|
+
22,
|
|
424
|
+
5,
|
|
425
|
+
9,
|
|
426
|
+
14,
|
|
427
|
+
20,
|
|
428
|
+
5,
|
|
429
|
+
9,
|
|
430
|
+
14,
|
|
431
|
+
20,
|
|
432
|
+
5,
|
|
433
|
+
9,
|
|
434
|
+
14,
|
|
435
|
+
20,
|
|
436
|
+
5,
|
|
437
|
+
9,
|
|
438
|
+
14,
|
|
439
|
+
20,
|
|
440
|
+
4,
|
|
441
|
+
11,
|
|
442
|
+
16,
|
|
443
|
+
23,
|
|
444
|
+
4,
|
|
445
|
+
11,
|
|
446
|
+
16,
|
|
447
|
+
23,
|
|
448
|
+
4,
|
|
449
|
+
11,
|
|
450
|
+
16,
|
|
451
|
+
23,
|
|
452
|
+
4,
|
|
453
|
+
11,
|
|
454
|
+
16,
|
|
455
|
+
23,
|
|
456
|
+
6,
|
|
457
|
+
10,
|
|
458
|
+
15,
|
|
459
|
+
21,
|
|
460
|
+
6,
|
|
461
|
+
10,
|
|
462
|
+
15,
|
|
463
|
+
21,
|
|
464
|
+
6,
|
|
465
|
+
10,
|
|
466
|
+
15,
|
|
467
|
+
21,
|
|
468
|
+
6,
|
|
469
|
+
10,
|
|
470
|
+
15,
|
|
471
|
+
21
|
|
472
|
+
];
|
|
473
|
+
var K = new Uint32Array([
|
|
474
|
+
3614090360,
|
|
475
|
+
3905402710,
|
|
476
|
+
606105819,
|
|
477
|
+
3250441966,
|
|
478
|
+
4118548399,
|
|
479
|
+
1200080426,
|
|
480
|
+
2821735955,
|
|
481
|
+
4249261313,
|
|
482
|
+
1770035416,
|
|
483
|
+
2336552879,
|
|
484
|
+
4294925233,
|
|
485
|
+
2304563134,
|
|
486
|
+
1804603682,
|
|
487
|
+
4254626195,
|
|
488
|
+
2792965006,
|
|
489
|
+
1236535329,
|
|
490
|
+
4129170786,
|
|
491
|
+
3225465664,
|
|
492
|
+
643717713,
|
|
493
|
+
3921069994,
|
|
494
|
+
3593408605,
|
|
495
|
+
38016083,
|
|
496
|
+
3634488961,
|
|
497
|
+
3889429448,
|
|
498
|
+
568446438,
|
|
499
|
+
3275163606,
|
|
500
|
+
4107603335,
|
|
501
|
+
1163531501,
|
|
502
|
+
2850285829,
|
|
503
|
+
4243563512,
|
|
504
|
+
1735328473,
|
|
505
|
+
2368359562,
|
|
506
|
+
4294588738,
|
|
507
|
+
2272392833,
|
|
508
|
+
1839030562,
|
|
509
|
+
4259657740,
|
|
510
|
+
2763975236,
|
|
511
|
+
1272893353,
|
|
512
|
+
4139469664,
|
|
513
|
+
3200236656,
|
|
514
|
+
681279174,
|
|
515
|
+
3936430074,
|
|
516
|
+
3572445317,
|
|
517
|
+
76029189,
|
|
518
|
+
3654602809,
|
|
519
|
+
3873151461,
|
|
520
|
+
530742520,
|
|
521
|
+
3299628645,
|
|
522
|
+
4096336452,
|
|
523
|
+
1126891415,
|
|
524
|
+
2878612391,
|
|
525
|
+
4237533241,
|
|
526
|
+
1700485571,
|
|
527
|
+
2399980690,
|
|
528
|
+
4293915773,
|
|
529
|
+
2240044497,
|
|
530
|
+
1873313359,
|
|
531
|
+
4264355552,
|
|
532
|
+
2734768916,
|
|
533
|
+
1309151649,
|
|
534
|
+
4149444226,
|
|
535
|
+
3174756917,
|
|
536
|
+
718787259,
|
|
537
|
+
3951481745
|
|
538
|
+
]);
|
|
539
|
+
function leftRotate(value, shift) {
|
|
540
|
+
return u32(value << shift | value >>> 32 - shift);
|
|
541
|
+
}
|
|
542
|
+
function padMessage(data) {
|
|
543
|
+
const bitLen = data.length * 8;
|
|
544
|
+
const padLen = (56 - (data.length + 1) % 64 + 64) % 64;
|
|
545
|
+
const totalLen = data.length + 1 + padLen + 8;
|
|
546
|
+
const padded = new Uint8Array(totalLen);
|
|
547
|
+
padded.set(data);
|
|
548
|
+
padded[data.length] = 128;
|
|
549
|
+
const view = new DataView(padded.buffer);
|
|
550
|
+
view.setUint32(totalLen - 8, bitLen >>> 0, true);
|
|
551
|
+
view.setUint32(totalLen - 4, Math.floor(bitLen / 4294967296), true);
|
|
552
|
+
return padded;
|
|
553
|
+
}
|
|
554
|
+
function hmacMD5(key, data) {
|
|
555
|
+
let k = key;
|
|
556
|
+
if (k.length > BLOCK_SIZE) {
|
|
557
|
+
k = md5(k);
|
|
558
|
+
}
|
|
559
|
+
const paddedKey = new Uint8Array(BLOCK_SIZE);
|
|
560
|
+
paddedKey.set(k);
|
|
561
|
+
const ipad = new Uint8Array(BLOCK_SIZE);
|
|
562
|
+
const opad = new Uint8Array(BLOCK_SIZE);
|
|
563
|
+
for (let i = 0;i < BLOCK_SIZE; i++) {
|
|
564
|
+
ipad[i] = (paddedKey[i] ?? 0) ^ 54;
|
|
565
|
+
opad[i] = (paddedKey[i] ?? 0) ^ 92;
|
|
566
|
+
}
|
|
567
|
+
const inner = new Uint8Array(ipad.length + data.length);
|
|
568
|
+
inner.set(ipad);
|
|
569
|
+
inner.set(data, ipad.length);
|
|
570
|
+
const innerHash = md5(inner);
|
|
571
|
+
const outer = new Uint8Array(opad.length + innerHash.length);
|
|
572
|
+
outer.set(opad);
|
|
573
|
+
outer.set(innerHash, opad.length);
|
|
574
|
+
return md5(outer);
|
|
575
|
+
}
|
|
576
|
+
function bytesToHex(bytes) {
|
|
577
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
578
|
+
}
|
|
579
|
+
async function computeCRAMMD5(challenge, user, pass) {
|
|
580
|
+
const challengeBytes = decodeBase64(challenge.trim());
|
|
581
|
+
const passBytes = encodeUtf8(pass);
|
|
582
|
+
const digest = hmacMD5(passBytes, challengeBytes);
|
|
583
|
+
const hex = bytesToHex(digest);
|
|
584
|
+
return encodeBase64(`${user} ${hex}`).replace(/\r\n/g, "");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/core/smtp.ts
|
|
588
|
+
class SMTPError extends Error {
|
|
589
|
+
code;
|
|
590
|
+
command;
|
|
591
|
+
response;
|
|
592
|
+
constructor(message, code, command, response) {
|
|
593
|
+
super(message);
|
|
594
|
+
this.code = code;
|
|
595
|
+
this.command = command;
|
|
596
|
+
this.response = response;
|
|
597
|
+
this.name = "SMTPError";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function encodeCommand(cmd) {
|
|
601
|
+
let line;
|
|
602
|
+
switch (cmd.type) {
|
|
603
|
+
case "EHLO":
|
|
604
|
+
line = `EHLO ${cmd.domain}`;
|
|
605
|
+
break;
|
|
606
|
+
case "STARTTLS":
|
|
607
|
+
line = "STARTTLS";
|
|
608
|
+
break;
|
|
609
|
+
case "AUTH_LOGIN":
|
|
610
|
+
line = "AUTH LOGIN";
|
|
611
|
+
break;
|
|
612
|
+
case "AUTH_PLAIN":
|
|
613
|
+
line = `AUTH PLAIN ${encodeBase64(`\x00${cmd.user}\x00${cmd.pass}`).replace(/\r\n/g, "")}`;
|
|
614
|
+
break;
|
|
615
|
+
case "AUTH_CRAM_MD5_INIT":
|
|
616
|
+
line = "AUTH CRAM-MD5";
|
|
617
|
+
break;
|
|
618
|
+
case "AUTH_CRAM_MD5_RESPONSE":
|
|
619
|
+
return encodeUtf8(`${cmd.response}\r
|
|
620
|
+
`);
|
|
621
|
+
case "AUTH_XOAUTH2":
|
|
622
|
+
line = `AUTH XOAUTH2 ${cmd.xoauth2String}`;
|
|
623
|
+
break;
|
|
624
|
+
case "MAIL_FROM":
|
|
625
|
+
line = `MAIL FROM:<${cmd.address}>`;
|
|
626
|
+
break;
|
|
627
|
+
case "RCPT_TO":
|
|
628
|
+
line = `RCPT TO:<${cmd.address}>`;
|
|
629
|
+
break;
|
|
630
|
+
case "DATA":
|
|
631
|
+
line = "DATA";
|
|
632
|
+
break;
|
|
633
|
+
case "DATA_BODY":
|
|
634
|
+
return encodeUtf8(applyDotStuffing(cmd.content));
|
|
635
|
+
case "QUIT":
|
|
636
|
+
line = "QUIT";
|
|
637
|
+
break;
|
|
638
|
+
case "RSET":
|
|
639
|
+
line = "RSET";
|
|
640
|
+
break;
|
|
641
|
+
case "NOOP":
|
|
642
|
+
line = "NOOP";
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
return encodeUtf8(`${line}\r
|
|
646
|
+
`);
|
|
647
|
+
}
|
|
648
|
+
function parseResponse(data) {
|
|
649
|
+
const text = new TextDecoder().decode(data).trim();
|
|
650
|
+
const lines = text.split(/\r?\n/);
|
|
651
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
652
|
+
const match = lastLine.match(/^(\d{3})([\s-])(.*)$/);
|
|
653
|
+
if (!match) {
|
|
654
|
+
throw new SMTPError("Invalid SMTP response", 0, "PARSE", text);
|
|
655
|
+
}
|
|
656
|
+
const code = Number.parseInt(match[1] ?? "0", 10);
|
|
657
|
+
const message = lines.map((l) => l.replace(/^\d{3}[\s-]/, "")).join(" ");
|
|
658
|
+
return {
|
|
659
|
+
code,
|
|
660
|
+
message,
|
|
661
|
+
isSuccess: code >= 200 && code < 300,
|
|
662
|
+
isReady: code >= 300 && code < 400,
|
|
663
|
+
isError: code >= 400
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function accumulateResponse(chunks) {
|
|
667
|
+
if (chunks.length === 0) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
671
|
+
const combined = new Uint8Array(total);
|
|
672
|
+
let offset = 0;
|
|
673
|
+
for (const chunk of chunks) {
|
|
674
|
+
combined.set(chunk, offset);
|
|
675
|
+
offset += chunk.length;
|
|
676
|
+
}
|
|
677
|
+
const text = new TextDecoder().decode(combined);
|
|
678
|
+
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
679
|
+
if (lines.length === 0) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
683
|
+
if (/^\d{3} /.test(lastLine)) {
|
|
684
|
+
return combined;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
function selectAuthMethod(capabilities) {
|
|
689
|
+
const upper = capabilities.map((c) => c.toUpperCase());
|
|
690
|
+
if (upper.some((c) => c.includes("AUTH") && c.includes("XOAUTH2"))) {
|
|
691
|
+
return "OAUTH2";
|
|
692
|
+
}
|
|
693
|
+
if (upper.some((c) => c.includes("AUTH") && c.includes("CRAM-MD5"))) {
|
|
694
|
+
return "CRAM-MD5";
|
|
695
|
+
}
|
|
696
|
+
if (upper.some((c) => c.includes("AUTH") && c.includes("LOGIN"))) {
|
|
697
|
+
return "LOGIN";
|
|
698
|
+
}
|
|
699
|
+
if (upper.some((c) => c.includes("AUTH") && c.includes("PLAIN"))) {
|
|
700
|
+
return "PLAIN";
|
|
701
|
+
}
|
|
702
|
+
throw new SMTPError("No supported AUTH method", 0, "EHLO", capabilities.join(" "));
|
|
703
|
+
}
|
|
704
|
+
function parseEHLO(response) {
|
|
705
|
+
return response.message.split(/\s+/).flatMap((part) => part.split(/\r?\n/)).filter(Boolean);
|
|
706
|
+
}
|
|
707
|
+
function assertResponse(response, expectedCodes, command) {
|
|
708
|
+
if (!expectedCodes.includes(response.code)) {
|
|
709
|
+
throw new SMTPError(`Unexpected SMTP response for ${command}`, response.code, command, response.message);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function applyDotStuffing(content) {
|
|
713
|
+
const text = new TextDecoder().decode(content);
|
|
714
|
+
const lines = text.split(/\r?\n/);
|
|
715
|
+
const stuffed = lines.map((line) => line.startsWith(".") ? `.${line}` : line);
|
|
716
|
+
return `${stuffed.join(`\r
|
|
717
|
+
`)}\r
|
|
718
|
+
.\r
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
function encodeAuthLoginPass(pass) {
|
|
722
|
+
return encodeUtf8(`${encodeBase64(pass).replace(/\r\n/g, "")}\r
|
|
723
|
+
`);
|
|
724
|
+
}
|
|
725
|
+
function encodeAuthLoginUser(user) {
|
|
726
|
+
return encodeUtf8(`${encodeBase64(user).replace(/\r\n/g, "")}\r
|
|
727
|
+
`);
|
|
728
|
+
}
|
|
729
|
+
function encodeLine(line) {
|
|
730
|
+
return encodeUtf8(`${line}\r
|
|
731
|
+
`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/transports/smtp.ts
|
|
735
|
+
class SMTPTransport {
|
|
736
|
+
config;
|
|
737
|
+
adapter = null;
|
|
738
|
+
constructor(config) {
|
|
739
|
+
this.config = resolveSMTPConfig(config);
|
|
740
|
+
}
|
|
741
|
+
async send(options) {
|
|
742
|
+
const resolvedOptions = {
|
|
743
|
+
...options,
|
|
744
|
+
attachments: await resolveAttachments(options.attachments)
|
|
745
|
+
};
|
|
746
|
+
const mime = await buildMIME(resolvedOptions, this.config.dkim);
|
|
747
|
+
const adapter = await this.getAdapter();
|
|
748
|
+
const host = this.config.direct ? await resolveMX(mime.envelope.from.split("@")[1] ?? this.config.host) : this.config.host;
|
|
749
|
+
await adapter.connect(host, this.config.port);
|
|
750
|
+
this.adapter = adapter;
|
|
751
|
+
try {
|
|
752
|
+
await openSMTPSession(adapter, this.config);
|
|
753
|
+
return await deliverSMTPMessage(adapter, mime);
|
|
754
|
+
} finally {
|
|
755
|
+
await closeSMTPSession(adapter);
|
|
756
|
+
this.adapter = null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async verify() {
|
|
760
|
+
const adapter = await this.getAdapter();
|
|
761
|
+
await adapter.connect(this.config.host, this.config.port);
|
|
762
|
+
try {
|
|
763
|
+
await openSMTPSession(adapter, this.config);
|
|
764
|
+
return true;
|
|
765
|
+
} finally {
|
|
766
|
+
await closeSMTPSession(adapter);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async close() {
|
|
770
|
+
if (this.adapter) {
|
|
771
|
+
await this.adapter.close();
|
|
772
|
+
this.adapter = null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async getAdapter() {
|
|
776
|
+
if (!this.config.adapter) {
|
|
777
|
+
throw new SMTPError("No socket adapter configured", 0, "CONNECT", "");
|
|
778
|
+
}
|
|
779
|
+
return this.config.adapter;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function resolveSMTPConfig(config) {
|
|
783
|
+
const secure = config.secure ?? false;
|
|
784
|
+
return {
|
|
785
|
+
host: config.host,
|
|
786
|
+
port: config.port ?? (secure ? 465 : 587),
|
|
787
|
+
secure,
|
|
788
|
+
...config.auth !== undefined ? { auth: config.auth } : {},
|
|
789
|
+
...config.dkim !== undefined ? { dkim: config.dkim } : {},
|
|
790
|
+
...config.tls !== undefined ? { tls: config.tls } : {},
|
|
791
|
+
...config.connectionTimeout !== undefined ? { connectionTimeout: config.connectionTimeout } : {},
|
|
792
|
+
...config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {},
|
|
793
|
+
...config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {},
|
|
794
|
+
...config.direct !== undefined ? { direct: config.direct } : {},
|
|
795
|
+
...config.adapter !== undefined ? { adapter: config.adapter } : {}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async function openSMTPSession(adapter, config) {
|
|
799
|
+
const greeting = await readSMTPResponse(adapter);
|
|
800
|
+
assertResponse(greeting, [220], "greeting");
|
|
801
|
+
let capabilities = await ehlo(adapter, config.host);
|
|
802
|
+
if (!config.secure && !adapter.secure) {
|
|
803
|
+
await sendRaw(adapter, encodeCommand({ type: "STARTTLS" }));
|
|
804
|
+
const starttlsResp = await readSMTPResponse(adapter);
|
|
805
|
+
assertResponse(starttlsResp, [220], "STARTTLS");
|
|
806
|
+
await adapter.startTLS(config.tls);
|
|
807
|
+
capabilities = await ehlo(adapter, config.host);
|
|
808
|
+
}
|
|
809
|
+
if (config.auth) {
|
|
810
|
+
await authenticate(adapter, config.auth, capabilities);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function deliverSMTPMessage(adapter, mime) {
|
|
814
|
+
await sendCommand(adapter, { type: "MAIL_FROM", address: mime.envelope.from });
|
|
815
|
+
const mailResp = await readSMTPResponse(adapter);
|
|
816
|
+
assertResponse(mailResp, [250], "MAIL FROM");
|
|
817
|
+
const accepted = [];
|
|
818
|
+
const rejected = [];
|
|
819
|
+
for (const recipient of mime.envelope.to) {
|
|
820
|
+
await sendRaw(adapter, encodeCommand({ type: "RCPT_TO", address: recipient }));
|
|
821
|
+
const rcptResp = await readSMTPResponse(adapter);
|
|
822
|
+
if (rcptResp.isSuccess) {
|
|
823
|
+
accepted.push(recipient);
|
|
824
|
+
} else {
|
|
825
|
+
rejected.push(recipient);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
await sendCommand(adapter, { type: "DATA" });
|
|
829
|
+
const dataResp = await readSMTPResponse(adapter);
|
|
830
|
+
assertResponse(dataResp, [354], "DATA");
|
|
831
|
+
let finalResp;
|
|
832
|
+
try {
|
|
833
|
+
await sendRaw(adapter, encodeCommand({ type: "DATA_BODY", content: mime.raw }));
|
|
834
|
+
finalResp = await readSMTPResponse(adapter);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
await sendRaw(adapter, encodeCommand({ type: "DATA_BODY", content: mime.raw }));
|
|
837
|
+
finalResp = await readSMTPResponse(adapter);
|
|
838
|
+
if (finalResp.isError) {
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
assertResponse(finalResp, [250], "DATA end");
|
|
843
|
+
return {
|
|
844
|
+
messageId: mime.messageId,
|
|
845
|
+
accepted,
|
|
846
|
+
rejected,
|
|
847
|
+
response: finalResp.message,
|
|
848
|
+
envelope: mime.envelope
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
async function closeSMTPSession(adapter) {
|
|
852
|
+
try {
|
|
853
|
+
await sendCommand(adapter, { type: "QUIT" });
|
|
854
|
+
await readSMTPResponse(adapter);
|
|
855
|
+
} catch {} finally {
|
|
856
|
+
await adapter.close();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function ehlo(adapter, host) {
|
|
860
|
+
await sendCommand(adapter, { type: "EHLO", domain: host });
|
|
861
|
+
const response = await readSMTPResponse(adapter);
|
|
862
|
+
assertResponse(response, [250], "EHLO");
|
|
863
|
+
return parseEHLO(response);
|
|
864
|
+
}
|
|
865
|
+
async function authenticate(adapter, auth, capabilities) {
|
|
866
|
+
if (auth.type === "OAUTH2" && auth.oauth2) {
|
|
867
|
+
const client = new OAuth2Client(auth.oauth2);
|
|
868
|
+
const xoauth2 = await client.buildXOAUTH2();
|
|
869
|
+
await sendCommand(adapter, { type: "AUTH_XOAUTH2", xoauth2String: xoauth2 });
|
|
870
|
+
let resp2 = await readSMTPResponse(adapter);
|
|
871
|
+
if (resp2.code === 334) {
|
|
872
|
+
await sendRaw(adapter, encodeLine(""));
|
|
873
|
+
resp2 = await readSMTPResponse(adapter);
|
|
874
|
+
}
|
|
875
|
+
assertResponse(resp2, [235], "AUTH XOAUTH2");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const method = auth.type ?? selectAuthMethod(capabilities);
|
|
879
|
+
if (method === "CRAM-MD5") {
|
|
880
|
+
const pass2 = requirePassword(auth, "CRAM-MD5");
|
|
881
|
+
await sendCommand(adapter, { type: "AUTH_CRAM_MD5_INIT" });
|
|
882
|
+
let resp2 = await readSMTPResponse(adapter);
|
|
883
|
+
assertResponse(resp2, [334], "AUTH CRAM-MD5");
|
|
884
|
+
const challenge = resp2.message.trim();
|
|
885
|
+
const response = await computeCRAMMD5(challenge, auth.user, pass2);
|
|
886
|
+
await sendCommand(adapter, { type: "AUTH_CRAM_MD5_RESPONSE", response });
|
|
887
|
+
resp2 = await readSMTPResponse(adapter);
|
|
888
|
+
assertResponse(resp2, [235], "AUTH CRAM-MD5 response");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (method === "PLAIN") {
|
|
892
|
+
const pass2 = requirePassword(auth, "PLAIN");
|
|
893
|
+
await sendRaw(adapter, encodeCommand({ type: "AUTH_PLAIN", user: auth.user, pass: pass2 }));
|
|
894
|
+
const resp2 = await readSMTPResponse(adapter);
|
|
895
|
+
assertResponse(resp2, [235], "AUTH PLAIN");
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const pass = requirePassword(auth, "LOGIN");
|
|
899
|
+
await sendRaw(adapter, encodeCommand({ type: "AUTH_LOGIN", user: auth.user, pass }));
|
|
900
|
+
let resp = await readSMTPResponse(adapter);
|
|
901
|
+
assertResponse(resp, [334], "AUTH LOGIN");
|
|
902
|
+
await sendRaw(adapter, encodeAuthLoginUser(auth.user));
|
|
903
|
+
resp = await readSMTPResponse(adapter);
|
|
904
|
+
assertResponse(resp, [334], "AUTH LOGIN user");
|
|
905
|
+
await sendRaw(adapter, encodeAuthLoginPass(pass));
|
|
906
|
+
resp = await readSMTPResponse(adapter);
|
|
907
|
+
assertResponse(resp, [235], "AUTH LOGIN pass");
|
|
908
|
+
}
|
|
909
|
+
function requirePassword(auth, method) {
|
|
910
|
+
if (!auth.pass) {
|
|
911
|
+
throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, "");
|
|
912
|
+
}
|
|
913
|
+
return auth.pass;
|
|
914
|
+
}
|
|
915
|
+
async function sendCommand(adapter, command) {
|
|
916
|
+
await sendRaw(adapter, encodeCommand(command));
|
|
917
|
+
}
|
|
918
|
+
async function sendRaw(adapter, data) {
|
|
919
|
+
await adapter.write(data);
|
|
920
|
+
}
|
|
921
|
+
async function readSMTPResponse(adapter) {
|
|
922
|
+
const chunks = [];
|
|
923
|
+
for await (const chunk of adapter.read()) {
|
|
924
|
+
chunks.push(chunk);
|
|
925
|
+
const complete = accumulateResponse(chunks);
|
|
926
|
+
if (complete) {
|
|
927
|
+
return parseResponse(complete);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
throw new SMTPError("Connection closed while reading SMTP response", 0, "READ", "");
|
|
931
|
+
}
|
|
932
|
+
async function resolveMX(domain) {
|
|
933
|
+
const dns = await import("node:dns/promises");
|
|
934
|
+
const records = await dns.resolveMx(domain);
|
|
935
|
+
if (records.length === 0) {
|
|
936
|
+
throw new SMTPError(`No MX records for ${domain}`, 0, "MX", "");
|
|
937
|
+
}
|
|
938
|
+
records.sort((a, b) => a.priority - b.priority);
|
|
939
|
+
return records[0]?.exchange ?? domain;
|
|
940
|
+
}
|
|
941
|
+
export { buildMIME, encodeLine, SMTPTransport, resolveSMTPConfig, openSMTPSession, deliverSMTPMessage, closeSMTPSession, readSMTPResponse };
|
|
942
|
+
|
|
943
|
+
//# debugId=6E41A48857F001F464756E2164756E21
|