@bumail/smtp 0.4.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 (42) hide show
  1. package/README.md +44 -5
  2. package/dist/chunks/{index-753q702a.js → index-n4xwep5m.js} +3 -3
  3. package/dist/chunks/{index-753q702a.js.map → index-n4xwep5m.js.map} +3 -3
  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 +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +550 -70
  10. package/dist/index.js.map +15 -8
  11. package/dist/server/admission.d.ts +29 -0
  12. package/dist/server/admission.d.ts.map +1 -0
  13. package/dist/server/listener.d.ts +17 -8
  14. package/dist/server/listener.d.ts.map +1 -1
  15. package/dist/server/options.d.ts +21 -1
  16. package/dist/server/options.d.ts.map +1 -1
  17. package/dist/server/proxy/address.d.ts +17 -0
  18. package/dist/server/proxy/address.d.ts.map +1 -0
  19. package/dist/server/proxy/header.d.ts +26 -0
  20. package/dist/server/proxy/header.d.ts.map +1 -0
  21. package/dist/server/proxy/reader.d.ts +15 -0
  22. package/dist/server/proxy/reader.d.ts.map +1 -0
  23. package/dist/server/proxy/tls.d.ts +50 -0
  24. package/dist/server/proxy/tls.d.ts.map +1 -0
  25. package/dist/server/proxy/trusted.d.ts +9 -0
  26. package/dist/server/proxy/trusted.d.ts.map +1 -0
  27. package/dist/server/raw-socket.d.ts +9 -0
  28. package/dist/server/raw-socket.d.ts.map +1 -0
  29. package/dist/server/received.d.ts +2 -0
  30. package/dist/server/received.d.ts.map +1 -1
  31. package/dist/server/server.d.ts.map +1 -1
  32. package/dist/server/settings.d.ts +2 -0
  33. package/dist/server/settings.d.ts.map +1 -1
  34. package/dist/server/tls-context.d.ts +16 -0
  35. package/dist/server/tls-context.d.ts.map +1 -0
  36. package/dist/server/transport.d.ts +2 -2
  37. package/dist/server/transport.d.ts.map +1 -1
  38. package/docs/README.md +2 -2
  39. package/docs/guide.md +345 -7
  40. package/docs/roadmap.md +13 -0
  41. package/docs/troubleshooting.md +322 -1
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  decodeLoginStep2,
8
8
  LineSplitter,
9
9
  Outgoing
10
- } from "./chunks/index-753q702a.js";
10
+ } from "./chunks/index-n4xwep5m.js";
11
11
  // src/protocol/command.ts
12
12
  function parseCommand(line) {
13
13
  const space = line.indexOf(" ");
@@ -474,6 +474,7 @@ async function runCommand(connection, line) {
474
474
  }
475
475
 
476
476
  // src/server/received.ts
477
+ import { isIPv6 } from "node:net";
477
478
  var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
478
479
  var MONTHS = [
479
480
  "Jan",
@@ -498,6 +499,10 @@ function protocolOf(esmtp, secure, authenticated) {
498
499
  return "SMTP";
499
500
  return `ESMTP${secure ? "S" : ""}${authenticated ? "A" : ""}`;
500
501
  }
502
+ function addressLiteral(address) {
503
+ const bare = address.replace(/%.*$/, "");
504
+ return isIPv6(bare) ? `IPv6:${bare}` : printable(address);
505
+ }
501
506
  function printable(text) {
502
507
  let out = "";
503
508
  for (const char of text) {
@@ -512,7 +517,7 @@ function plain(address) {
512
517
  function receivedField(connection, recipients, id, now = new Date) {
513
518
  const { state, transport, settings } = connection;
514
519
  const lines = [
515
- `Received: from ${printable(state.helo ?? "unknown")} ([${printable(transport.remoteAddress)}])`,
520
+ `Received: from ${printable(state.helo ?? "unknown")} ([${addressLiteral(transport.remoteAddress)}])`,
516
521
  ` by ${settings.options.hostname} with ${protocolOf(state.esmtp, state.secure, state.user !== undefined)} id ${id}`
517
522
  ];
518
523
  const [recipient] = recipients;
@@ -1124,19 +1129,33 @@ class SocketTransport {
1124
1129
  }
1125
1130
 
1126
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
+ }
1127
1137
  function handlers(settings) {
1128
1138
  const idle = reply(421, "4.4.2", `${settings.options.hostname} Idle too long, closing`);
1129
1139
  return {
1130
1140
  data(socket, chunk) {
1131
- if (!socket.data || socket.data.upgraded)
1141
+ const state = socket.data;
1142
+ if (!state || state.upgraded)
1132
1143
  return;
1133
- socket.data.transport?.received();
1134
- socket.timeout(settings.timeout);
1135
- 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);
1136
1150
  },
1137
1151
  drain(socket) {
1138
- if (socket.data && !socket.data.upgraded)
1139
- 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();
1140
1159
  },
1141
1160
  error(socket) {
1142
1161
  socket.data?.connection?.close();
@@ -1154,14 +1173,14 @@ function handlers(settings) {
1154
1173
  }
1155
1174
  };
1156
1175
  }
1157
- function turnAway(socket, secure, line) {
1158
- const refused = new SocketTransport(socket, secure, () => {});
1176
+ function turnAway(socket, raw, secure, line) {
1177
+ const refused = new SocketTransport(raw, secure, () => {});
1159
1178
  socket.data.transport = refused;
1160
1179
  refused.write(line);
1161
1180
  refused.end();
1162
1181
  }
1163
- function transportOf(socket, settings, secure, slots) {
1164
- const transport = new SocketTransport(socket, secure, () => {
1182
+ function transportOf(socket, raw, address, settings, secure, slots) {
1183
+ const transport = new SocketTransport(raw, secure, () => {
1165
1184
  const tls = settings.options.tls;
1166
1185
  const connection = socket.data.connection;
1167
1186
  if (!tls || !connection)
@@ -1180,53 +1199,341 @@ function transportOf(socket, settings, secure, slots) {
1180
1199
  }
1181
1200
  });
1182
1201
  encrypted.timeout(settings.timeout);
1183
- const next = new SocketTransport(encrypted, true, () => {}, socket.remoteAddress);
1202
+ const next = new SocketTransport(encrypted, true, () => {}, address);
1184
1203
  encrypted.data.transport = next;
1185
1204
  connection.useTransport(next);
1186
- });
1205
+ }, address);
1187
1206
  socket.data.transport = transport;
1188
1207
  return transport;
1189
1208
  }
1190
- function listenerHandlers(settings, slots, handshaking, secure) {
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;
1191
1522
  return {
1192
1523
  open(socket) {
1193
1524
  socket.data = { upgraded: false, handshaking: secure };
1194
1525
  socket.timeout(secure ? settings.handshakeTimeout : settings.timeout);
1195
- const begin = (start) => {
1196
- if (!secure)
1197
- return start();
1198
- handshaking.add(socket);
1199
- socket.data.start = start;
1200
- };
1201
- if (slots.size >= settings.maxConnections) {
1202
- return begin(() => turnAway(socket, secure, `421 4.3.2 ${settings.options.hostname} Too many connections, try later\r
1203
- `));
1204
- }
1205
- const key = clientKey(socket.remoteAddress);
1206
- if (slots.of(key) >= settings.maxConnectionsPerClient) {
1207
- return begin(() => turnAway(socket, secure, `421 4.7.0 ${settings.options.hostname} Too many connections from your address, try later\r
1208
- `));
1209
- }
1210
- const connection = new Connection(settings, transportOf(socket, settings, secure, slots));
1211
- slots.take(connection, key);
1212
- socket.data.connection = connection;
1213
- begin(() => void connection.open());
1214
- },
1215
- handshake(socket, success) {
1216
- const state = socket.data;
1217
- if (!state?.handshaking)
1218
- return;
1219
- handshaking.delete(socket);
1220
- if (!success)
1221
- return;
1222
- state.handshaking = false;
1223
- socket.timeout(settings.timeout);
1224
- const start = state.start;
1225
- delete state.start;
1226
- start?.();
1526
+ const peer = socket.remoteAddress;
1527
+ if (settings.trusts?.(peer))
1528
+ awaitHeader(listening, socket, peer);
1529
+ else
1530
+ admit(listening, socket, peer);
1227
1531
  },
1532
+ handshake: (socket, success) => handshake(listening, socket, success),
1228
1533
  close(socket) {
1229
1534
  handshaking.delete(socket);
1535
+ socket.data?.header?.cancel();
1536
+ socket.data?.tls?.closed();
1230
1537
  const connection = socket.data?.connection;
1231
1538
  if (connection)
1232
1539
  slots.release(connection);
@@ -1235,22 +1542,151 @@ function listenerHandlers(settings, slots, handshaking, secure) {
1235
1542
  }
1236
1543
  };
1237
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
+ }
1238
1674
 
1239
1675
  // src/server/settings.ts
1240
1676
  var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
1241
- var invalid = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
1677
+ var invalid2 = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
1242
1678
  function positive(name, value, fallback) {
1243
1679
  if (value === undefined)
1244
1680
  return fallback;
1245
1681
  if (!Number.isSafeInteger(value) || value < 1) {
1246
- throw invalid(`${name} must be a positive integer, not ${value}`);
1682
+ throw invalid2(`${name} must be a positive integer, not ${value}`);
1247
1683
  }
1248
1684
  return value;
1249
1685
  }
1250
1686
  function timer(name, value, fallback) {
1251
1687
  const seconds = positive(name, value, fallback);
1252
1688
  if (seconds > MAX_TIMER_SECONDS) {
1253
- throw invalid(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
1689
+ throw invalid2(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
1254
1690
  }
1255
1691
  return seconds;
1256
1692
  }
@@ -1258,7 +1694,7 @@ function nonNegative(name, value, fallback) {
1258
1694
  if (value === undefined)
1259
1695
  return fallback;
1260
1696
  if (!Number.isFinite(value) || value < 0) {
1261
- throw invalid(`${name} must be a number of seconds, 0 or more, not ${value}`);
1697
+ throw invalid2(`${name} must be a number of seconds, 0 or more, not ${value}`);
1262
1698
  }
1263
1699
  return value;
1264
1700
  }
@@ -1269,29 +1705,29 @@ function localLookup(local) {
1269
1705
  const domains = new Set(local.map((domain) => domain.toLowerCase()));
1270
1706
  return (domain) => domains.has(domain);
1271
1707
  }
1272
- throw invalid("localDomains must be an array of domains or a function");
1708
+ throw invalid2("localDomains must be an array of domains or a function");
1273
1709
  }
1274
1710
  function settingsOf(options) {
1275
1711
  if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
1276
- throw invalid(`"${options.hostname}" is not a host name`);
1712
+ throw invalid2(`"${options.hostname}" is not a host name`);
1277
1713
  }
1278
1714
  if (typeof options.onData !== "function") {
1279
- throw invalid("onData must be a function: it is where messages go");
1715
+ throw invalid2("onData must be a function: it is where messages go");
1280
1716
  }
1281
1717
  if (options.implicitTls && !options.tls) {
1282
- throw invalid("implicitTls needs tls: { key, cert }");
1718
+ throw invalid2("implicitTls needs tls: { key, cert }");
1283
1719
  }
1284
1720
  if (options.mode === "submission" && !options.authenticate) {
1285
- throw invalid("submission takes mail only from authenticated users, so it needs authenticate");
1721
+ throw invalid2("submission takes mail only from authenticated users, so it needs authenticate");
1286
1722
  }
1287
1723
  if (options.authenticate && !options.tls) {
1288
- throw invalid("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1724
+ throw invalid2("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1289
1725
  }
1290
1726
  const isLocal = localLookup(options.localDomains);
1291
1727
  const timeout = positive("timeout", options.timeout, 300);
1292
1728
  const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
1293
1729
  if (greetingDelay >= timeout) {
1294
- throw invalid(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1730
+ throw invalid2(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1295
1731
  }
1296
1732
  return {
1297
1733
  options,
@@ -1305,6 +1741,7 @@ function settingsOf(options) {
1305
1741
  hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
1306
1742
  handshakeTimeout: positive("handshakeTimeout", options.handshakeTimeout, 10),
1307
1743
  greetingDelay,
1744
+ ...options.proxyProtocol === undefined ? {} : { trusts: trustedPeers(options.proxyProtocol?.trusted, invalid2) },
1308
1745
  isLocal: (domain) => isLocal(domain.toLowerCase())
1309
1746
  };
1310
1747
  }
@@ -1342,33 +1779,76 @@ class Slots {
1342
1779
  }
1343
1780
  }
1344
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
+
1345
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
+ }
1346
1819
  function createSmtpServer(options) {
1347
1820
  const settings = settingsOf(options);
1348
1821
  let listener;
1349
1822
  const slots = new Slots;
1350
1823
  const handshaking = new Set;
1351
1824
  const secure = options.implicitTls === true;
1825
+ let starting = false;
1826
+ let stopRequested = false;
1352
1827
  return {
1353
1828
  get connections() {
1354
1829
  return slots.size;
1355
1830
  },
1356
1831
  async listen({ port, hostname = "0.0.0.0" }) {
1357
- if (listener) {
1358
- 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");
1359
1834
  }
1360
- listener = Bun.listen({
1361
- hostname,
1362
- port,
1363
- ...secure && options.tls ? { tls: { key: options.tls.key, cert: options.tls.cert } } : {},
1364
- socket: {
1365
- ...handlers(settings),
1366
- ...listenerHandlers(settings, slots, handshaking, secure)
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);
1367
1842
  }
1368
- });
1369
- return { port: listener.port, hostname: listener.hostname };
1843
+ listener = bound;
1844
+ return { port: listener.port, hostname: listener.hostname };
1845
+ } finally {
1846
+ starting = false;
1847
+ }
1370
1848
  },
1371
1849
  stop(closeConnections = false) {
1850
+ if (starting)
1851
+ stopRequested = true;
1372
1852
  listener?.stop(closeConnections);
1373
1853
  listener = undefined;
1374
1854
  if (!closeConnections)
@@ -1400,5 +1880,5 @@ export {
1400
1880
  reply
1401
1881
  };
1402
1882
 
1403
- //# debugId=388406E838895A3964756E2164756E21
1883
+ //# debugId=C3E2F4573FDEF0E964756E2164756E21
1404
1884
  //# sourceMappingURL=index.js.map