@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.
- package/README.md +44 -5
- package/dist/chunks/{index-753q702a.js → index-n4xwep5m.js} +3 -3
- package/dist/chunks/{index-753q702a.js.map → index-n4xwep5m.js.map} +3 -3
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -70
- package/dist/index.js.map +15 -8
- package/dist/server/admission.d.ts +29 -0
- package/dist/server/admission.d.ts.map +1 -0
- package/dist/server/listener.d.ts +17 -8
- package/dist/server/listener.d.ts.map +1 -1
- package/dist/server/options.d.ts +21 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/proxy/address.d.ts +17 -0
- package/dist/server/proxy/address.d.ts.map +1 -0
- package/dist/server/proxy/header.d.ts +26 -0
- package/dist/server/proxy/header.d.ts.map +1 -0
- package/dist/server/proxy/reader.d.ts +15 -0
- package/dist/server/proxy/reader.d.ts.map +1 -0
- package/dist/server/proxy/tls.d.ts +50 -0
- package/dist/server/proxy/tls.d.ts.map +1 -0
- package/dist/server/proxy/trusted.d.ts +9 -0
- package/dist/server/proxy/trusted.d.ts.map +1 -0
- package/dist/server/raw-socket.d.ts +9 -0
- package/dist/server/raw-socket.d.ts.map +1 -0
- package/dist/server/received.d.ts +2 -0
- package/dist/server/received.d.ts.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/settings.d.ts +2 -0
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/tls-context.d.ts +16 -0
- package/dist/server/tls-context.d.ts.map +1 -0
- package/dist/server/transport.d.ts +2 -2
- package/dist/server/transport.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide.md +345 -7
- package/docs/roadmap.md +13 -0
- package/docs/troubleshooting.md +322 -1
- 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-
|
|
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")} ([${
|
|
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
|
-
|
|
1141
|
+
const state = socket.data;
|
|
1142
|
+
if (!state || state.upgraded)
|
|
1132
1143
|
return;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
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(
|
|
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(
|
|
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, () => {},
|
|
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
|
-
|
|
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
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
socket
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1712
|
+
throw invalid2(`"${options.hostname}" is not a host name`);
|
|
1277
1713
|
}
|
|
1278
1714
|
if (typeof options.onData !== "function") {
|
|
1279
|
-
throw
|
|
1715
|
+
throw invalid2("onData must be a function: it is where messages go");
|
|
1280
1716
|
}
|
|
1281
1717
|
if (options.implicitTls && !options.tls) {
|
|
1282
|
-
throw
|
|
1718
|
+
throw invalid2("implicitTls needs tls: { key, cert }");
|
|
1283
1719
|
}
|
|
1284
1720
|
if (options.mode === "submission" && !options.authenticate) {
|
|
1285
|
-
throw
|
|
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
|
|
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
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
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=
|
|
1883
|
+
//# debugId=C3E2F4573FDEF0E964756E2164756E21
|
|
1404
1884
|
//# sourceMappingURL=index.js.map
|