@bumail/smtp 0.3.0 → 0.5.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.
Files changed (52) hide show
  1. package/README.md +105 -7
  2. package/dist/chunks/{index-3423jxf9.js → index-n4xwep5m.js} +10 -4
  3. package/dist/chunks/index-n4xwep5m.js.map +14 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/errors.d.ts +3 -1
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +724 -126
  10. package/dist/index.js.map +19 -9
  11. package/dist/protocol/command.d.ts +4 -2
  12. package/dist/protocol/command.d.ts.map +1 -1
  13. package/dist/protocol/path.d.ts +13 -2
  14. package/dist/protocol/path.d.ts.map +1 -1
  15. package/dist/server/admission.d.ts +29 -0
  16. package/dist/server/admission.d.ts.map +1 -0
  17. package/dist/server/client-key.d.ts +15 -0
  18. package/dist/server/client-key.d.ts.map +1 -0
  19. package/dist/server/listener.d.ts +46 -0
  20. package/dist/server/listener.d.ts.map +1 -0
  21. package/dist/server/options.d.ts +45 -1
  22. package/dist/server/options.d.ts.map +1 -1
  23. package/dist/server/proxy/address.d.ts +17 -0
  24. package/dist/server/proxy/address.d.ts.map +1 -0
  25. package/dist/server/proxy/header.d.ts +26 -0
  26. package/dist/server/proxy/header.d.ts.map +1 -0
  27. package/dist/server/proxy/reader.d.ts +15 -0
  28. package/dist/server/proxy/reader.d.ts.map +1 -0
  29. package/dist/server/proxy/tls.d.ts +50 -0
  30. package/dist/server/proxy/tls.d.ts.map +1 -0
  31. package/dist/server/proxy/trusted.d.ts +9 -0
  32. package/dist/server/proxy/trusted.d.ts.map +1 -0
  33. package/dist/server/raw-socket.d.ts +9 -0
  34. package/dist/server/raw-socket.d.ts.map +1 -0
  35. package/dist/server/received.d.ts +2 -0
  36. package/dist/server/received.d.ts.map +1 -1
  37. package/dist/server/server.d.ts.map +1 -1
  38. package/dist/server/settings.d.ts +4 -0
  39. package/dist/server/settings.d.ts.map +1 -1
  40. package/dist/server/slots.d.ts +14 -0
  41. package/dist/server/slots.d.ts.map +1 -0
  42. package/dist/server/tls-context.d.ts +16 -0
  43. package/dist/server/tls-context.d.ts.map +1 -0
  44. package/dist/server/transaction.d.ts.map +1 -1
  45. package/dist/server/transport.d.ts +2 -2
  46. package/dist/server/transport.d.ts.map +1 -1
  47. package/docs/README.md +2 -2
  48. package/docs/guide.md +472 -12
  49. package/docs/roadmap.md +57 -0
  50. package/docs/troubleshooting.md +406 -4
  51. package/package.json +2 -2
  52. package/dist/chunks/index-3423jxf9.js.map +0 -14
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  SmtpError2,
3
+ POSTMASTER,
3
4
  isHelloName,
4
5
  parsePath2,
5
6
  decodePlain2,
6
7
  decodeLoginStep2,
7
8
  LineSplitter,
8
9
  Outgoing
9
- } from "./chunks/index-3423jxf9.js";
10
+ } from "./chunks/index-n4xwep5m.js";
10
11
  // src/protocol/command.ts
11
12
  function parseCommand(line) {
12
13
  const space = line.indexOf(" ");
@@ -21,7 +22,8 @@ function parsePathCommand(argument, keyword) {
21
22
  const match = new RegExp(`^${keyword}:\\s*(<[^>]*>)(.*)$`, "i").exec(argument);
22
23
  if (!match)
23
24
  return;
24
- const path = parsePath2(match[1], keyword === "FROM");
25
+ const text = match[1];
26
+ const path = keyword === "TO" && text.toLowerCase() === "<postmaster>" ? POSTMASTER : parsePath2(text, keyword === "FROM");
25
27
  if (!path)
26
28
  return;
27
29
  const parameters = {};
@@ -123,6 +125,40 @@ function formatReply(reply, enhanced = true) {
123
125
  }).join("");
124
126
  }
125
127
  var reply = (code, status, text) => status === undefined ? { code, text } : { code, status, text };
128
+ // src/server/client-key.ts
129
+ import { isIP } from "node:net";
130
+ function groups(ip) {
131
+ const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(ip);
132
+ let text = ip;
133
+ if (dotted !== null) {
134
+ const [a, b, c, d] = dotted.slice(1).map(Number);
135
+ text = `${ip.slice(0, dotted.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
136
+ }
137
+ const [head = "", tail] = text.split("::");
138
+ const left = head === "" ? [] : head.split(":");
139
+ const right = tail === undefined || tail === "" ? [] : tail.split(":");
140
+ const all = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
141
+ return all.map((group) => Number.parseInt(group, 16));
142
+ }
143
+ function ipv4Of(g) {
144
+ const [high = 0, low = 0] = g.slice(6);
145
+ return [high >> 8, high & 255, low >> 8, low & 255].join(".");
146
+ }
147
+ function clientKey(ip) {
148
+ if (typeof ip !== "string")
149
+ return;
150
+ const address = ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
151
+ if (isIP(address) === 4)
152
+ return address;
153
+ if (isIP(address) !== 6)
154
+ return;
155
+ const g = groups(address.toLowerCase());
156
+ const prefix = g.slice(0, 6).join(":");
157
+ if (prefix === "0:0:0:0:0:65535" || prefix === "100:65435:0:0:0:0") {
158
+ return ipv4Of(g);
159
+ }
160
+ return `${g.slice(0, 4).map((group) => group.toString(16)).join(":")}::/64`;
161
+ }
126
162
  // src/server/guard.ts
127
163
  var LOCAL_ERROR = reply(451, "4.3.0", "Local error in processing");
128
164
  var TIMED_OUT = Symbol("timed out");
@@ -326,7 +362,7 @@ async function rcpt(connection, argument) {
326
362
  if (state.transaction.to.length >= settings.maxRecipients) {
327
363
  return connection.send(reply(452, "4.5.3", "Too many recipients"));
328
364
  }
329
- if (state.user === undefined) {
365
+ if (state.user === undefined && path.postmaster !== true) {
330
366
  const local = await connection.check("localDomains", () => settings.isLocal(path.domain));
331
367
  if (local.failed)
332
368
  return connection.send(LOCAL_ERROR);
@@ -438,6 +474,7 @@ async function runCommand(connection, line) {
438
474
  }
439
475
 
440
476
  // src/server/received.ts
477
+ import { isIPv6 } from "node:net";
441
478
  var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
442
479
  var MONTHS = [
443
480
  "Jan",
@@ -462,6 +499,10 @@ function protocolOf(esmtp, secure, authenticated) {
462
499
  return "SMTP";
463
500
  return `ESMTP${secure ? "S" : ""}${authenticated ? "A" : ""}`;
464
501
  }
502
+ function addressLiteral(address) {
503
+ const bare = address.replace(/%.*$/, "");
504
+ return isIPv6(bare) ? `IPv6:${bare}` : printable(address);
505
+ }
465
506
  function printable(text) {
466
507
  let out = "";
467
508
  for (const char of text) {
@@ -476,7 +517,7 @@ function plain(address) {
476
517
  function receivedField(connection, recipients, id, now = new Date) {
477
518
  const { state, transport, settings } = connection;
478
519
  const lines = [
479
- `Received: from ${printable(state.helo ?? "unknown")} ([${printable(transport.remoteAddress)}])`,
520
+ `Received: from ${printable(state.helo ?? "unknown")} ([${addressLiteral(transport.remoteAddress)}])`,
480
521
  ` by ${settings.options.hostname} with ${protocolOf(state.esmtp, state.secure, state.user !== undefined)} id ${id}`
481
522
  ];
482
523
  const [recipient] = recipients;
@@ -952,77 +993,6 @@ class Connection {
952
993
  }
953
994
  }
954
995
 
955
- // src/server/settings.ts
956
- var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
957
- var invalid = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
958
- function positive(name, value, fallback) {
959
- if (value === undefined)
960
- return fallback;
961
- if (!Number.isSafeInteger(value) || value < 1) {
962
- throw invalid(`${name} must be a positive integer, not ${value}`);
963
- }
964
- return value;
965
- }
966
- function timer(name, value, fallback) {
967
- const seconds = positive(name, value, fallback);
968
- if (seconds > MAX_TIMER_SECONDS) {
969
- throw invalid(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
970
- }
971
- return seconds;
972
- }
973
- function nonNegative(name, value, fallback) {
974
- if (value === undefined)
975
- return fallback;
976
- if (!Number.isFinite(value) || value < 0) {
977
- throw invalid(`${name} must be a number of seconds, 0 or more, not ${value}`);
978
- }
979
- return value;
980
- }
981
- function localLookup(local) {
982
- if (typeof local === "function")
983
- return local;
984
- if (Array.isArray(local) && local.every((domain) => typeof domain === "string")) {
985
- const domains = new Set(local.map((domain) => domain.toLowerCase()));
986
- return (domain) => domains.has(domain);
987
- }
988
- throw invalid("localDomains must be an array of domains or a function");
989
- }
990
- function settingsOf(options) {
991
- if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
992
- throw invalid(`"${options.hostname}" is not a host name`);
993
- }
994
- if (typeof options.onData !== "function") {
995
- throw invalid("onData must be a function: it is where messages go");
996
- }
997
- if (options.implicitTls && !options.tls) {
998
- throw invalid("implicitTls needs tls: { key, cert }");
999
- }
1000
- if (options.mode === "submission" && !options.authenticate) {
1001
- throw invalid("submission takes mail only from authenticated users, so it needs authenticate");
1002
- }
1003
- if (options.authenticate && !options.tls) {
1004
- throw invalid("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1005
- }
1006
- const isLocal = localLookup(options.localDomains);
1007
- const timeout = positive("timeout", options.timeout, 300);
1008
- const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
1009
- if (greetingDelay >= timeout) {
1010
- throw invalid(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1011
- }
1012
- return {
1013
- options,
1014
- mode: options.mode ?? "mx",
1015
- maxMessageSize: positive("maxMessageSize", options.maxMessageSize, 25 * 1024 * 1024),
1016
- maxRecipients: positive("maxRecipients", options.maxRecipients, 100),
1017
- maxConnections: positive("maxConnections", options.maxConnections, 1000),
1018
- maxErrors: positive("maxErrors", options.maxErrors, 10),
1019
- timeout,
1020
- hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
1021
- greetingDelay,
1022
- isLocal: (domain) => isLocal(domain.toLowerCase())
1023
- };
1024
- }
1025
-
1026
996
  // src/server/transport.ts
1027
997
  var CLOSE_GRACE_MS = 5000;
1028
998
  var LINGER_QUIET_MS = 20;
@@ -1158,27 +1128,43 @@ class SocketTransport {
1158
1128
  }
1159
1129
  }
1160
1130
 
1161
- // src/server/server.ts
1131
+ // src/server/listener.ts
1132
+ function received(socket, settings, chunk) {
1133
+ socket.data.transport?.received();
1134
+ socket.timeout(settings.timeout);
1135
+ socket.data.connection?.receive(chunk);
1136
+ }
1162
1137
  function handlers(settings) {
1163
1138
  const idle = reply(421, "4.4.2", `${settings.options.hostname} Idle too long, closing`);
1164
1139
  return {
1165
1140
  data(socket, chunk) {
1166
- if (socket.data.upgraded)
1141
+ const state = socket.data;
1142
+ if (!state || state.upgraded)
1167
1143
  return;
1168
- socket.data.transport?.received();
1169
- socket.timeout(settings.timeout);
1170
- socket.data.connection?.receive(chunk);
1144
+ if (state.header)
1145
+ return state.header.receive(chunk);
1146
+ if (!state.tls)
1147
+ return received(socket, settings, chunk);
1148
+ state.transport?.received();
1149
+ state.tls.receive(chunk);
1171
1150
  },
1172
1151
  drain(socket) {
1173
- if (!socket.data.upgraded)
1174
- socket.data.transport?.drain();
1152
+ const state = socket.data;
1153
+ if (!state || state.upgraded)
1154
+ return;
1155
+ if (state.tls)
1156
+ state.tls.rawDrain();
1157
+ else
1158
+ state.transport?.drain();
1175
1159
  },
1176
1160
  error(socket) {
1177
- socket.data.connection?.close();
1161
+ socket.data?.connection?.close();
1178
1162
  },
1179
1163
  timeout(socket) {
1180
- if (socket.data.upgraded)
1164
+ if (!socket.data || socket.data.upgraded)
1181
1165
  return;
1166
+ if (socket.data.handshaking)
1167
+ return socket.terminate();
1182
1168
  const { connection, transport } = socket.data;
1183
1169
  if (connection?.closed)
1184
1170
  transport?.abort();
@@ -1187,8 +1173,14 @@ function handlers(settings) {
1187
1173
  }
1188
1174
  };
1189
1175
  }
1190
- function transportOf(socket, settings, secure) {
1191
- const transport = new SocketTransport(socket, secure, () => {
1176
+ function turnAway(socket, raw, secure, line) {
1177
+ const refused = new SocketTransport(raw, secure, () => {});
1178
+ socket.data.transport = refused;
1179
+ refused.write(line);
1180
+ refused.end();
1181
+ }
1182
+ function transportOf(socket, raw, address, settings, secure, slots) {
1183
+ const transport = new SocketTransport(raw, secure, () => {
1192
1184
  const tls = settings.options.tls;
1193
1185
  const connection = socket.data.connection;
1194
1186
  if (!tls || !connection)
@@ -1200,78 +1192,684 @@ function transportOf(socket, settings, secure) {
1200
1192
  socket: {
1201
1193
  ...handlers(settings),
1202
1194
  close: (s) => {
1195
+ slots.release(connection);
1203
1196
  s.data.transport?.closed();
1204
1197
  connection.close();
1205
1198
  }
1206
1199
  }
1207
1200
  });
1208
1201
  encrypted.timeout(settings.timeout);
1209
- const next = new SocketTransport(encrypted, true, () => {}, socket.remoteAddress);
1202
+ const next = new SocketTransport(encrypted, true, () => {}, address);
1210
1203
  encrypted.data.transport = next;
1211
1204
  connection.useTransport(next);
1212
- });
1205
+ }, address);
1213
1206
  socket.data.transport = transport;
1214
1207
  return transport;
1215
1208
  }
1209
+
1210
+ // src/server/proxy/address.ts
1211
+ import { isIP as isIP2 } from "node:net";
1212
+ function addressBytes(text) {
1213
+ if (text.includes("%"))
1214
+ return;
1215
+ const kind = isIP2(text);
1216
+ if (kind === 4)
1217
+ return new Uint8Array(text.split(".").map(Number));
1218
+ if (kind !== 6)
1219
+ return;
1220
+ return unmapped(ipv6Bytes(text.toLowerCase()));
1221
+ }
1222
+ function ipv6Bytes(text) {
1223
+ const bytes = new Uint8Array(16);
1224
+ let tail = text;
1225
+ const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
1226
+ if (dotted) {
1227
+ bytes.set(dotted.slice(1).map(Number), 12);
1228
+ tail = `${text.slice(0, dotted.index)}0:0`;
1229
+ }
1230
+ const [head = "", rest] = tail.split("::");
1231
+ const left = head === "" ? [] : head.split(":");
1232
+ const right = rest === undefined || rest === "" ? [] : rest.split(":");
1233
+ const groups = rest === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
1234
+ const view = new DataView(bytes.buffer);
1235
+ groups.forEach((group, at) => {
1236
+ if (!dotted || at < 6)
1237
+ view.setUint16(at * 2, Number.parseInt(group, 16));
1238
+ });
1239
+ return bytes;
1240
+ }
1241
+ function unmapped(bytes) {
1242
+ if (bytes.length !== 16)
1243
+ return bytes;
1244
+ const mapped = bytes.subarray(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255;
1245
+ return mapped ? bytes.slice(12) : bytes;
1246
+ }
1247
+ function formatAddress(raw) {
1248
+ const bytes = unmapped(raw);
1249
+ if (bytes.length === 4)
1250
+ return bytes.join(".");
1251
+ const view = new DataView(bytes.buffer, bytes.byteOffset, 16);
1252
+ const groups = Array.from({ length: 8 }, (_, at) => view.getUint16(at * 2));
1253
+ let best = { at: -1, length: 1 };
1254
+ for (let at = 0;at < 8; ) {
1255
+ let end = at;
1256
+ while (end < 8 && groups[end] === 0)
1257
+ end++;
1258
+ if (end - at > best.length)
1259
+ best = { at, length: end - at };
1260
+ at = end === at ? at + 1 : end;
1261
+ }
1262
+ const hex = groups.map((group) => group.toString(16));
1263
+ if (best.at < 0)
1264
+ return hex.join(":");
1265
+ const left = hex.slice(0, best.at).join(":");
1266
+ const right = hex.slice(best.at + best.length).join(":");
1267
+ return `${left}::${right}`;
1268
+ }
1269
+
1270
+ // src/server/proxy/header.ts
1271
+ var MAX_V1_LENGTH = 107;
1272
+ var MAX_TLV_BYTES = 2048;
1273
+ var MAX_HEADER_LENGTH = 16 + 216 + MAX_TLV_BYTES;
1274
+ var V1_PREFIX = new TextEncoder().encode("PROXY ");
1275
+ var V2_SIGNATURE = new Uint8Array([
1276
+ 13,
1277
+ 10,
1278
+ 13,
1279
+ 10,
1280
+ 0,
1281
+ 13,
1282
+ 10,
1283
+ 81,
1284
+ 85,
1285
+ 73,
1286
+ 84,
1287
+ 10
1288
+ ]);
1289
+ var ADDRESS_LENGTH = [0, 12, 36, 216];
1290
+ var PARTIAL = { status: "partial" };
1291
+ var invalid = (reason) => ({ status: "invalid", reason });
1292
+ function startsAs(bytes, prefix) {
1293
+ const length = Math.min(bytes.length, prefix.length);
1294
+ for (let at = 0;at < length; at++) {
1295
+ if (bytes[at] !== prefix[at])
1296
+ return false;
1297
+ }
1298
+ return true;
1299
+ }
1300
+ function readProxyHeader(bytes) {
1301
+ if (bytes.length === 0)
1302
+ return PARTIAL;
1303
+ if (startsAs(bytes, V2_SIGNATURE))
1304
+ return readV2(bytes);
1305
+ if (startsAs(bytes, V1_PREFIX))
1306
+ return readV1(bytes);
1307
+ return invalid("no PROXY protocol signature");
1308
+ }
1309
+ function readV2(bytes) {
1310
+ if (bytes.length < 16)
1311
+ return PARTIAL;
1312
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1313
+ const versionCommand = view.getUint8(12);
1314
+ if (versionCommand >> 4 !== 2)
1315
+ return invalid("not version 2");
1316
+ const command = versionCommand & 15;
1317
+ if (command > 1)
1318
+ return invalid(`unknown command ${command}`);
1319
+ const family = view.getUint8(13) >> 4;
1320
+ const transport = view.getUint8(13) & 15;
1321
+ const length = view.getUint16(14);
1322
+ const addresses = command === 0 ? 0 : ADDRESS_LENGTH[family];
1323
+ if (addresses === undefined || transport > 2) {
1324
+ return invalid("unknown address family or transport");
1325
+ }
1326
+ if (length < addresses)
1327
+ return invalid("address block too short");
1328
+ if (length - addresses > MAX_TLV_BYTES)
1329
+ return invalid("TLVs too long");
1330
+ const total = 16 + length;
1331
+ if (bytes.length < total)
1332
+ return PARTIAL;
1333
+ if (command === 0)
1334
+ return { status: "complete", length: total };
1335
+ const tlvs = bytes.subarray(16 + addresses, total);
1336
+ if (!wellFormedTlvs(tlvs))
1337
+ return invalid("malformed TLV");
1338
+ if (transport !== 1 || family !== 1 && family !== 2) {
1339
+ return { status: "complete", length: total };
1340
+ }
1341
+ const size = family === 1 ? 4 : 16;
1342
+ const source = formatAddress(bytes.slice(16, 16 + size));
1343
+ return { status: "complete", length: total, source };
1344
+ }
1345
+ function wellFormedTlvs(block) {
1346
+ let at = 0;
1347
+ while (at < block.length) {
1348
+ if (at + 3 > block.length)
1349
+ return false;
1350
+ const length = block[at + 1] << 8 | block[at + 2];
1351
+ at += 3 + length;
1352
+ }
1353
+ return at === block.length;
1354
+ }
1355
+ var PORT = /^(0|[1-9]\d{0,4})$/;
1356
+ function readV1(bytes) {
1357
+ const end = bytes.indexOf(10);
1358
+ if (end < 0) {
1359
+ return bytes.length >= MAX_V1_LENGTH ? invalid("v1 line too long") : PARTIAL;
1360
+ }
1361
+ if (end + 1 > MAX_V1_LENGTH)
1362
+ return invalid("v1 line too long");
1363
+ if (bytes[end - 1] !== 13)
1364
+ return invalid("v1 line without CRLF");
1365
+ const line = bytes.subarray(0, end - 1);
1366
+ if (line.some((byte) => byte < 32 || byte > 126)) {
1367
+ return invalid("v1 line not printable");
1368
+ }
1369
+ const [, protocol, ...rest] = new TextDecoder().decode(line).split(" ");
1370
+ if (protocol === "UNKNOWN")
1371
+ return { status: "complete", length: end + 1 };
1372
+ const [source = "", destination = "", sourcePort = "", port = ""] = rest;
1373
+ const size = protocol === "TCP4" ? 4 : protocol === "TCP6" ? 16 : 0;
1374
+ if (size === 0)
1375
+ return invalid("v1 protocol not TCP4, TCP6 or UNKNOWN");
1376
+ const from = rawAddress(source, size);
1377
+ if (rest.length !== 4 || !from || !rawAddress(destination, size) || !validPort(sourcePort) || !validPort(port)) {
1378
+ return invalid("v1 addresses or ports malformed");
1379
+ }
1380
+ return { status: "complete", length: end + 1, source: formatAddress(from) };
1381
+ }
1382
+ function rawAddress(text, size) {
1383
+ if (size === 4 && text.includes(":"))
1384
+ return;
1385
+ if (size === 16 && !text.includes(":"))
1386
+ return;
1387
+ return addressBytes(text);
1388
+ }
1389
+ function validPort(text) {
1390
+ return PORT.test(text) && Number(text) <= 65535;
1391
+ }
1392
+
1393
+ // src/server/proxy/reader.ts
1394
+ class HeaderReader {
1395
+ #bytes = new Uint8Array(0);
1396
+ #timer;
1397
+ #settled = false;
1398
+ #done;
1399
+ #refuse;
1400
+ constructor(seconds, done, refuse) {
1401
+ this.#done = done;
1402
+ this.#refuse = refuse;
1403
+ this.#timer = setTimeout(() => this.#settle(refuse), seconds * 1000);
1404
+ this.#timer.unref?.();
1405
+ }
1406
+ receive(chunk) {
1407
+ if (this.#settled)
1408
+ return;
1409
+ const bytes = new Uint8Array(this.#bytes.length + chunk.length);
1410
+ bytes.set(this.#bytes);
1411
+ bytes.set(chunk, this.#bytes.length);
1412
+ this.#bytes = bytes;
1413
+ const header = readProxyHeader(bytes);
1414
+ if (header.status === "complete") {
1415
+ const rest = bytes.slice(header.length);
1416
+ this.#settle(() => this.#done(header.source, rest));
1417
+ } else if (header.status === "invalid" || bytes.length >= MAX_HEADER_LENGTH) {
1418
+ this.#settle(this.#refuse);
1419
+ }
1420
+ }
1421
+ cancel() {
1422
+ this.#settled = true;
1423
+ clearTimeout(this.#timer);
1424
+ this.#bytes = new Uint8Array(0);
1425
+ }
1426
+ #settle(then) {
1427
+ if (this.#settled)
1428
+ return;
1429
+ this.cancel();
1430
+ then();
1431
+ }
1432
+ }
1433
+
1434
+ // src/server/proxy/tls.ts
1435
+ import { Duplex } from "node:stream";
1436
+ import { TLSSocket } from "node:tls";
1437
+
1438
+ class ProxiedTls {
1439
+ #raw;
1440
+ #wire;
1441
+ #tls;
1442
+ #pending;
1443
+ #halfClose = true;
1444
+ constructor(raw, context, events) {
1445
+ this.#raw = raw;
1446
+ this.#wire = new Duplex({
1447
+ read() {},
1448
+ write: (chunk, _encoding, done) => this.#send(chunk, done),
1449
+ final: (done) => {
1450
+ this.#raw.shutdown(this.#halfClose);
1451
+ done();
1452
+ }
1453
+ });
1454
+ this.#tls = new TLSSocket(this.#wire, {
1455
+ isServer: true,
1456
+ secureContext: context
1457
+ });
1458
+ this.#tls.on("secure", () => events.secure());
1459
+ this.#tls.on("data", (bytes) => events.data(bytes));
1460
+ this.#tls.on("drain", () => events.drain());
1461
+ this.#tls.on("error", () => this.terminate());
1462
+ }
1463
+ get remoteAddress() {
1464
+ return this.#raw.remoteAddress;
1465
+ }
1466
+ receive(chunk) {
1467
+ this.#wire.push(Uint8Array.from(chunk));
1468
+ }
1469
+ rawDrain() {
1470
+ const pending = this.#pending;
1471
+ if (!pending)
1472
+ return;
1473
+ const written = Math.max(this.#raw.write(pending.bytes), 0);
1474
+ if (written < pending.bytes.length) {
1475
+ pending.bytes = pending.bytes.subarray(written);
1476
+ return;
1477
+ }
1478
+ this.#pending = undefined;
1479
+ pending.done();
1480
+ }
1481
+ #send(chunk, done) {
1482
+ const written = Math.max(this.#raw.write(chunk), 0);
1483
+ if (written < chunk.length) {
1484
+ this.#pending = { bytes: chunk.subarray(written), done };
1485
+ } else
1486
+ done();
1487
+ }
1488
+ write(bytes) {
1489
+ if (this.#tls.destroyed || this.#tls.writableNeedDrain)
1490
+ return 0;
1491
+ this.#tls.write(bytes);
1492
+ return bytes.length;
1493
+ }
1494
+ shutdown(halfClose = false) {
1495
+ this.#halfClose = halfClose;
1496
+ this.#tls.once("finish", () => this.#wire.end());
1497
+ this.#tls.end();
1498
+ }
1499
+ terminate() {
1500
+ this.#pending = undefined;
1501
+ this.#tls.destroy();
1502
+ this.#raw.terminate();
1503
+ }
1504
+ closed() {
1505
+ this.#pending = undefined;
1506
+ this.#tls.destroy();
1507
+ }
1508
+ pause() {
1509
+ this.#raw.pause();
1510
+ }
1511
+ resume() {
1512
+ this.#raw.resume();
1513
+ }
1514
+ timeout(seconds) {
1515
+ this.#raw.timeout(seconds);
1516
+ }
1517
+ }
1518
+
1519
+ // src/server/admission.ts
1520
+ function listenerHandlers(listening) {
1521
+ const { settings, slots, handshaking, secure } = listening;
1522
+ return {
1523
+ open(socket) {
1524
+ socket.data = { upgraded: false, handshaking: secure };
1525
+ socket.timeout(secure ? settings.handshakeTimeout : settings.timeout);
1526
+ const peer = socket.remoteAddress;
1527
+ if (settings.trusts?.(peer))
1528
+ awaitHeader(listening, socket, peer);
1529
+ else
1530
+ admit(listening, socket, peer);
1531
+ },
1532
+ handshake: (socket, success) => handshake(listening, socket, success),
1533
+ close(socket) {
1534
+ handshaking.delete(socket);
1535
+ socket.data?.header?.cancel();
1536
+ socket.data?.tls?.closed();
1537
+ const connection = socket.data?.connection;
1538
+ if (connection)
1539
+ slots.release(connection);
1540
+ socket.data?.transport?.closed();
1541
+ connection?.close();
1542
+ }
1543
+ };
1544
+ }
1545
+ function handshake({ settings, handshaking }, socket, success) {
1546
+ const state = socket.data;
1547
+ if (!state?.handshaking)
1548
+ return;
1549
+ handshaking.delete(socket);
1550
+ if (!success)
1551
+ return;
1552
+ state.handshaking = false;
1553
+ socket.timeout(settings.timeout);
1554
+ const start = state.start;
1555
+ delete state.start;
1556
+ start?.();
1557
+ }
1558
+ function awaitHeader(listening, socket, peer) {
1559
+ const { settings, handshaking, secure } = listening;
1560
+ socket.data.handshaking = true;
1561
+ socket.timeout(settings.handshakeTimeout);
1562
+ handshaking.add(socket);
1563
+ socket.data.header = new HeaderReader(settings.handshakeTimeout, (source, rest) => {
1564
+ delete socket.data.header;
1565
+ handshaking.delete(socket);
1566
+ socket.data.handshaking = secure;
1567
+ if (!secure)
1568
+ socket.timeout(settings.timeout);
1569
+ admit(listening, socket, source ?? peer, rest);
1570
+ }, () => socket.terminate());
1571
+ }
1572
+ function admit(listening, socket, address, first) {
1573
+ const { settings, slots, handshaking, secure, proxied } = listening;
1574
+ let raw = socket;
1575
+ if (proxied) {
1576
+ raw = startTls2(listening, socket, proxied);
1577
+ if (first && first.length > 0)
1578
+ socket.data.tls?.receive(first);
1579
+ }
1580
+ const begin = (start) => {
1581
+ if (!secure)
1582
+ return start();
1583
+ handshaking.add(socket);
1584
+ socket.data.start = start;
1585
+ };
1586
+ const { hostname } = settings.options;
1587
+ const refuse = (line) => begin(() => turnAway(socket, raw, secure, `${line}\r
1588
+ `));
1589
+ if (slots.size >= settings.maxConnections) {
1590
+ refuse(`421 4.3.2 ${hostname} Too many connections, try later`);
1591
+ return;
1592
+ }
1593
+ const key = clientKey(address);
1594
+ if (slots.of(key) >= settings.maxConnectionsPerClient) {
1595
+ refuse(`421 4.7.0 ${hostname} Too many connections from your address, try later`);
1596
+ return;
1597
+ }
1598
+ const transport = transportOf(socket, raw, address, settings, secure, slots);
1599
+ const connection = new Connection(settings, transport);
1600
+ slots.take(connection, key);
1601
+ socket.data.connection = connection;
1602
+ if (first && first.length > 0 && !proxied) {
1603
+ received(socket, settings, first);
1604
+ }
1605
+ begin(() => void connection.open());
1606
+ }
1607
+ function startTls2(listening, socket, context) {
1608
+ const { settings } = listening;
1609
+ socket.data.handshaking = true;
1610
+ socket.timeout(settings.handshakeTimeout);
1611
+ const tls = new ProxiedTls(socket, context, {
1612
+ secure: () => handshake(listening, socket, true),
1613
+ data: (bytes) => received(socket, settings, bytes),
1614
+ drain: () => socket.data.transport?.drain()
1615
+ });
1616
+ socket.data.tls = tls;
1617
+ return tls;
1618
+ }
1619
+
1620
+ // src/server/proxy/trusted.ts
1621
+ function networkOf(entry) {
1622
+ if (typeof entry !== "string")
1623
+ return `${String(entry)} is not a string`;
1624
+ const [address = "", prefix, extra] = entry.split("/");
1625
+ const raw = addressBytes(address);
1626
+ if (!raw || extra !== undefined) {
1627
+ return `"${entry}" is neither an IP address nor a CIDR`;
1628
+ }
1629
+ const mapped = raw.length === 4 && address.includes(":");
1630
+ const width = raw.length * 8;
1631
+ if (prefix === undefined)
1632
+ return { bytes: raw, bits: width };
1633
+ let bits = /^\d{1,3}$/.test(prefix) ? Number(prefix) : -1;
1634
+ if (mapped)
1635
+ bits -= 96;
1636
+ if (bits < 0 || bits > width) {
1637
+ return `"${entry}" has a prefix length out of range`;
1638
+ }
1639
+ if (bits === 0) {
1640
+ return `"${entry}" has a prefix length of 0, which trusts every peer`;
1641
+ }
1642
+ return { bytes: raw, bits };
1643
+ }
1644
+ function contains(network, address) {
1645
+ if (network.bytes.length !== address.length)
1646
+ return false;
1647
+ const whole = network.bits >> 3;
1648
+ for (let at = 0;at < whole; at++) {
1649
+ if (network.bytes[at] !== address[at])
1650
+ return false;
1651
+ }
1652
+ const rest = network.bits & 7;
1653
+ if (rest === 0)
1654
+ return true;
1655
+ const mask = 255 << 8 - rest & 255;
1656
+ return (network.bytes[whole] & mask) === (address[whole] & mask);
1657
+ }
1658
+ function trustedPeers(entries, invalid) {
1659
+ if (!Array.isArray(entries) || entries.length === 0) {
1660
+ throw invalid("proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one");
1661
+ }
1662
+ const networks = entries.map((entry) => {
1663
+ const network = networkOf(entry);
1664
+ if (typeof network === "string") {
1665
+ throw invalid(`proxyProtocol.trusted: ${network}`);
1666
+ }
1667
+ return network;
1668
+ });
1669
+ return (peer) => {
1670
+ const address = addressBytes(peer);
1671
+ return address !== undefined && networks.some((network) => contains(network, address));
1672
+ };
1673
+ }
1674
+
1675
+ // src/server/settings.ts
1676
+ var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
1677
+ var invalid2 = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
1678
+ function positive(name, value, fallback) {
1679
+ if (value === undefined)
1680
+ return fallback;
1681
+ if (!Number.isSafeInteger(value) || value < 1) {
1682
+ throw invalid2(`${name} must be a positive integer, not ${value}`);
1683
+ }
1684
+ return value;
1685
+ }
1686
+ function timer(name, value, fallback) {
1687
+ const seconds = positive(name, value, fallback);
1688
+ if (seconds > MAX_TIMER_SECONDS) {
1689
+ throw invalid2(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
1690
+ }
1691
+ return seconds;
1692
+ }
1693
+ function nonNegative(name, value, fallback) {
1694
+ if (value === undefined)
1695
+ return fallback;
1696
+ if (!Number.isFinite(value) || value < 0) {
1697
+ throw invalid2(`${name} must be a number of seconds, 0 or more, not ${value}`);
1698
+ }
1699
+ return value;
1700
+ }
1701
+ function localLookup(local) {
1702
+ if (typeof local === "function")
1703
+ return local;
1704
+ if (Array.isArray(local) && local.every((domain) => typeof domain === "string")) {
1705
+ const domains = new Set(local.map((domain) => domain.toLowerCase()));
1706
+ return (domain) => domains.has(domain);
1707
+ }
1708
+ throw invalid2("localDomains must be an array of domains or a function");
1709
+ }
1710
+ function settingsOf(options) {
1711
+ if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
1712
+ throw invalid2(`"${options.hostname}" is not a host name`);
1713
+ }
1714
+ if (typeof options.onData !== "function") {
1715
+ throw invalid2("onData must be a function: it is where messages go");
1716
+ }
1717
+ if (options.implicitTls && !options.tls) {
1718
+ throw invalid2("implicitTls needs tls: { key, cert }");
1719
+ }
1720
+ if (options.mode === "submission" && !options.authenticate) {
1721
+ throw invalid2("submission takes mail only from authenticated users, so it needs authenticate");
1722
+ }
1723
+ if (options.authenticate && !options.tls) {
1724
+ throw invalid2("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1725
+ }
1726
+ const isLocal = localLookup(options.localDomains);
1727
+ const timeout = positive("timeout", options.timeout, 300);
1728
+ const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
1729
+ if (greetingDelay >= timeout) {
1730
+ throw invalid2(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1731
+ }
1732
+ return {
1733
+ options,
1734
+ mode: options.mode ?? "mx",
1735
+ maxMessageSize: positive("maxMessageSize", options.maxMessageSize, 25 * 1024 * 1024),
1736
+ maxRecipients: positive("maxRecipients", options.maxRecipients, 100),
1737
+ maxConnections: positive("maxConnections", options.maxConnections, 1000),
1738
+ maxConnectionsPerClient: positive("maxConnectionsPerClient", options.maxConnectionsPerClient, 10),
1739
+ maxErrors: positive("maxErrors", options.maxErrors, 10),
1740
+ timeout,
1741
+ hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
1742
+ handshakeTimeout: positive("handshakeTimeout", options.handshakeTimeout, 10),
1743
+ greetingDelay,
1744
+ ...options.proxyProtocol === undefined ? {} : { trusts: trustedPeers(options.proxyProtocol?.trusted, invalid2) },
1745
+ isLocal: (domain) => isLocal(domain.toLowerCase())
1746
+ };
1747
+ }
1748
+
1749
+ // src/server/slots.ts
1750
+ class Slots {
1751
+ #open = new Map;
1752
+ #perClient = new Map;
1753
+ get size() {
1754
+ return this.#open.size;
1755
+ }
1756
+ of(key) {
1757
+ return key === undefined ? 0 : this.#perClient.get(key) ?? 0;
1758
+ }
1759
+ take(connection, key) {
1760
+ this.#open.set(connection, key);
1761
+ if (key !== undefined)
1762
+ this.#perClient.set(key, this.of(key) + 1);
1763
+ }
1764
+ release(connection) {
1765
+ if (!this.#open.has(connection))
1766
+ return;
1767
+ const key = this.#open.get(connection);
1768
+ this.#open.delete(connection);
1769
+ if (key === undefined)
1770
+ return;
1771
+ const left = this.of(key) - 1;
1772
+ if (left > 0)
1773
+ this.#perClient.set(key, left);
1774
+ else
1775
+ this.#perClient.delete(key);
1776
+ }
1777
+ connections() {
1778
+ return [...this.#open.keys()];
1779
+ }
1780
+ }
1781
+
1782
+ // src/server/tls-context.ts
1783
+ import { createSecureContext } from "node:tls";
1784
+ async function tlsContext(tls, fail) {
1785
+ const read = async (value) => typeof value === "string" ? value : Buffer.from(value instanceof Uint8Array ? value : await value.bytes());
1786
+ try {
1787
+ return createSecureContext({
1788
+ key: await read(tls.key),
1789
+ cert: await read(tls.cert)
1790
+ });
1791
+ } catch (cause) {
1792
+ const reason = cause instanceof Error ? cause.message : String(cause);
1793
+ throw fail(`listen(): tls: { key, cert } cannot be used: ${reason}`, cause);
1794
+ }
1795
+ }
1796
+
1797
+ // src/server/server.ts
1798
+ var invalidTls = (message, cause) => new SmtpError2("INVALID_OPTION", message, { cause });
1799
+ var STOPPED = "listen(): stop() was called before the server bound its port";
1800
+ async function bind(options, listening, port, hostname, stopped) {
1801
+ const context = listening.secure && options.tls ? await tlsContext(options.tls, invalidTls) : undefined;
1802
+ if (stopped()) {
1803
+ throw new SmtpError2("STOPPED", STOPPED);
1804
+ }
1805
+ const proxied = listening.settings.trusts ? context : undefined;
1806
+ return Bun.listen({
1807
+ hostname,
1808
+ port,
1809
+ ...listening.secure && options.tls && !proxied ? { tls: { key: options.tls.key, cert: options.tls.cert } } : {},
1810
+ socket: {
1811
+ ...handlers(listening.settings),
1812
+ ...listenerHandlers({
1813
+ ...listening,
1814
+ ...proxied ? { proxied } : {}
1815
+ })
1816
+ }
1817
+ });
1818
+ }
1216
1819
  function createSmtpServer(options) {
1217
1820
  const settings = settingsOf(options);
1218
1821
  let listener;
1219
- const open = new Set;
1822
+ const slots = new Slots;
1823
+ const handshaking = new Set;
1220
1824
  const secure = options.implicitTls === true;
1825
+ let starting = false;
1826
+ let stopRequested = false;
1221
1827
  return {
1222
1828
  get connections() {
1223
- return open.size;
1829
+ return slots.size;
1224
1830
  },
1225
1831
  async listen({ port, hostname = "0.0.0.0" }) {
1226
- if (listener) {
1227
- throw new SmtpError2("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
1832
+ if (listener || starting) {
1833
+ throw new SmtpError2("ALREADY_LISTENING", listener ? `listen(): the server is already listening on ${listener.hostname}:${listener.port}` : "listen(): the server is already starting to listen");
1228
1834
  }
1229
- listener = Bun.listen({
1230
- hostname,
1231
- port,
1232
- ...secure && options.tls ? { tls: { key: options.tls.key, cert: options.tls.cert } } : {},
1233
- socket: {
1234
- ...handlers(settings),
1235
- open(socket) {
1236
- socket.data = { upgraded: false };
1237
- socket.timeout(settings.timeout);
1238
- if (open.size >= settings.maxConnections) {
1239
- const refused = new SocketTransport(socket, secure, () => {});
1240
- socket.data.transport = refused;
1241
- refused.write(`421 4.3.2 ${options.hostname} Too many connections, try later\r
1242
- `);
1243
- refused.end();
1244
- return;
1245
- }
1246
- const connection = new Connection(settings, transportOf(socket, settings, secure));
1247
- open.add(connection);
1248
- socket.data.connection = connection;
1249
- connection.open();
1250
- },
1251
- close(socket) {
1252
- const connection = socket.data.connection;
1253
- if (connection)
1254
- open.delete(connection);
1255
- socket.data.transport?.closed();
1256
- socket.data.connection?.close();
1257
- }
1835
+ starting = true;
1836
+ stopRequested = false;
1837
+ try {
1838
+ const bound = await bind(options, { settings, slots, handshaking, secure }, port, hostname, () => stopRequested);
1839
+ if (stopRequested) {
1840
+ bound.stop(true);
1841
+ throw new SmtpError2("STOPPED", STOPPED);
1258
1842
  }
1259
- });
1260
- return { port: listener.port, hostname: listener.hostname };
1843
+ listener = bound;
1844
+ return { port: listener.port, hostname: listener.hostname };
1845
+ } finally {
1846
+ starting = false;
1847
+ }
1261
1848
  },
1262
1849
  stop(closeConnections = false) {
1850
+ if (starting)
1851
+ stopRequested = true;
1263
1852
  listener?.stop(closeConnections);
1264
1853
  listener = undefined;
1265
1854
  if (!closeConnections)
1266
1855
  return;
1267
- for (const connection of [...open])
1268
- connection.close();
1856
+ const stuck = new Set;
1857
+ for (const socket of [...handshaking]) {
1858
+ stuck.add(socket.data?.connection);
1859
+ handshaking.delete(socket);
1860
+ socket.terminate();
1861
+ }
1862
+ for (const connection of slots.connections()) {
1863
+ if (!stuck.has(connection))
1864
+ connection.close();
1865
+ }
1269
1866
  }
1270
1867
  };
1271
1868
  }
1272
1869
  export {
1273
1870
  DataReader,
1274
1871
  SmtpError2 as SmtpError,
1872
+ clientKey,
1275
1873
  createSmtpServer,
1276
1874
  decodeLoginStep2 as decodeLoginStep,
1277
1875
  decodePlain2 as decodePlain,
@@ -1282,5 +1880,5 @@ export {
1282
1880
  reply
1283
1881
  };
1284
1882
 
1285
- //# debugId=04EE37708209636464756E2164756E21
1883
+ //# debugId=C3E2F4573FDEF0E964756E2164756E21
1286
1884
  //# sourceMappingURL=index.js.map