@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.
- package/README.md +105 -7
- package/dist/chunks/{index-3423jxf9.js → index-n4xwep5m.js} +10 -4
- package/dist/chunks/index-n4xwep5m.js.map +14 -0
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +724 -126
- package/dist/index.js.map +19 -9
- package/dist/protocol/command.d.ts +4 -2
- package/dist/protocol/command.d.ts.map +1 -1
- package/dist/protocol/path.d.ts +13 -2
- package/dist/protocol/path.d.ts.map +1 -1
- package/dist/server/admission.d.ts +29 -0
- package/dist/server/admission.d.ts.map +1 -0
- package/dist/server/client-key.d.ts +15 -0
- package/dist/server/client-key.d.ts.map +1 -0
- package/dist/server/listener.d.ts +46 -0
- package/dist/server/listener.d.ts.map +1 -0
- package/dist/server/options.d.ts +45 -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 +4 -0
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/slots.d.ts +14 -0
- package/dist/server/slots.d.ts.map +1 -0
- package/dist/server/tls-context.d.ts +16 -0
- package/dist/server/tls-context.d.ts.map +1 -0
- package/dist/server/transaction.d.ts.map +1 -1
- 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 +472 -12
- package/docs/roadmap.md +57 -0
- package/docs/troubleshooting.md +406 -4
- package/package.json +2 -2
- 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-
|
|
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
|
|
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")} ([${
|
|
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/
|
|
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
|
-
|
|
1141
|
+
const state = socket.data;
|
|
1142
|
+
if (!state || state.upgraded)
|
|
1167
1143
|
return;
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
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
|
|
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
|
|
1191
|
-
const
|
|
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, () => {},
|
|
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
|
|
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
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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=
|
|
1883
|
+
//# debugId=C3E2F4573FDEF0E964756E2164756E21
|
|
1286
1884
|
//# sourceMappingURL=index.js.map
|