@bumail/smtp 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  SmtpError2,
3
+ POSTMASTER,
3
4
  isHelloName,
4
5
  parsePath2,
5
6
  decodePlain2,
6
7
  decodeLoginStep2,
7
8
  LineSplitter,
8
9
  Outgoing
9
- } from "./chunks/index-3423jxf9.js";
10
+ } from "./chunks/index-753q702a.js";
10
11
  // src/protocol/command.ts
11
12
  function parseCommand(line) {
12
13
  const space = line.indexOf(" ");
@@ -21,7 +22,8 @@ function parsePathCommand(argument, keyword) {
21
22
  const match = new RegExp(`^${keyword}:\\s*(<[^>]*>)(.*)$`, "i").exec(argument);
22
23
  if (!match)
23
24
  return;
24
- const path = parsePath2(match[1], keyword === "FROM");
25
+ const text = match[1];
26
+ const path = keyword === "TO" && text.toLowerCase() === "<postmaster>" ? POSTMASTER : parsePath2(text, keyword === "FROM");
25
27
  if (!path)
26
28
  return;
27
29
  const parameters = {};
@@ -123,6 +125,40 @@ function formatReply(reply, enhanced = true) {
123
125
  }).join("");
124
126
  }
125
127
  var reply = (code, status, text) => status === undefined ? { code, text } : { code, status, text };
128
+ // src/server/client-key.ts
129
+ import { isIP } from "node:net";
130
+ function groups(ip) {
131
+ const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(ip);
132
+ let text = ip;
133
+ if (dotted !== null) {
134
+ const [a, b, c, d] = dotted.slice(1).map(Number);
135
+ text = `${ip.slice(0, dotted.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
136
+ }
137
+ const [head = "", tail] = text.split("::");
138
+ const left = head === "" ? [] : head.split(":");
139
+ const right = tail === undefined || tail === "" ? [] : tail.split(":");
140
+ const all = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
141
+ return all.map((group) => Number.parseInt(group, 16));
142
+ }
143
+ function ipv4Of(g) {
144
+ const [high = 0, low = 0] = g.slice(6);
145
+ return [high >> 8, high & 255, low >> 8, low & 255].join(".");
146
+ }
147
+ function clientKey(ip) {
148
+ if (typeof ip !== "string")
149
+ return;
150
+ const address = ip.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
151
+ if (isIP(address) === 4)
152
+ return address;
153
+ if (isIP(address) !== 6)
154
+ return;
155
+ const g = groups(address.toLowerCase());
156
+ const prefix = g.slice(0, 6).join(":");
157
+ if (prefix === "0:0:0:0:0:65535" || prefix === "100:65435:0:0:0:0") {
158
+ return ipv4Of(g);
159
+ }
160
+ return `${g.slice(0, 4).map((group) => group.toString(16)).join(":")}::/64`;
161
+ }
126
162
  // src/server/guard.ts
127
163
  var LOCAL_ERROR = reply(451, "4.3.0", "Local error in processing");
128
164
  var TIMED_OUT = Symbol("timed out");
@@ -326,7 +362,7 @@ async function rcpt(connection, argument) {
326
362
  if (state.transaction.to.length >= settings.maxRecipients) {
327
363
  return connection.send(reply(452, "4.5.3", "Too many recipients"));
328
364
  }
329
- if (state.user === undefined) {
365
+ if (state.user === undefined && path.postmaster !== true) {
330
366
  const local = await connection.check("localDomains", () => settings.isLocal(path.domain));
331
367
  if (local.failed)
332
368
  return connection.send(LOCAL_ERROR);
@@ -952,77 +988,6 @@ class Connection {
952
988
  }
953
989
  }
954
990
 
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
991
  // src/server/transport.ts
1027
992
  var CLOSE_GRACE_MS = 5000;
1028
993
  var LINGER_QUIET_MS = 20;
@@ -1158,27 +1123,29 @@ class SocketTransport {
1158
1123
  }
1159
1124
  }
1160
1125
 
1161
- // src/server/server.ts
1126
+ // src/server/listener.ts
1162
1127
  function handlers(settings) {
1163
1128
  const idle = reply(421, "4.4.2", `${settings.options.hostname} Idle too long, closing`);
1164
1129
  return {
1165
1130
  data(socket, chunk) {
1166
- if (socket.data.upgraded)
1131
+ if (!socket.data || socket.data.upgraded)
1167
1132
  return;
1168
1133
  socket.data.transport?.received();
1169
1134
  socket.timeout(settings.timeout);
1170
1135
  socket.data.connection?.receive(chunk);
1171
1136
  },
1172
1137
  drain(socket) {
1173
- if (!socket.data.upgraded)
1138
+ if (socket.data && !socket.data.upgraded)
1174
1139
  socket.data.transport?.drain();
1175
1140
  },
1176
1141
  error(socket) {
1177
- socket.data.connection?.close();
1142
+ socket.data?.connection?.close();
1178
1143
  },
1179
1144
  timeout(socket) {
1180
- if (socket.data.upgraded)
1145
+ if (!socket.data || socket.data.upgraded)
1181
1146
  return;
1147
+ if (socket.data.handshaking)
1148
+ return socket.terminate();
1182
1149
  const { connection, transport } = socket.data;
1183
1150
  if (connection?.closed)
1184
1151
  transport?.abort();
@@ -1187,7 +1154,13 @@ function handlers(settings) {
1187
1154
  }
1188
1155
  };
1189
1156
  }
1190
- function transportOf(socket, settings, secure) {
1157
+ function turnAway(socket, secure, line) {
1158
+ const refused = new SocketTransport(socket, secure, () => {});
1159
+ socket.data.transport = refused;
1160
+ refused.write(line);
1161
+ refused.end();
1162
+ }
1163
+ function transportOf(socket, settings, secure, slots) {
1191
1164
  const transport = new SocketTransport(socket, secure, () => {
1192
1165
  const tls = settings.options.tls;
1193
1166
  const connection = socket.data.connection;
@@ -1200,6 +1173,7 @@ function transportOf(socket, settings, secure) {
1200
1173
  socket: {
1201
1174
  ...handlers(settings),
1202
1175
  close: (s) => {
1176
+ slots.release(connection);
1203
1177
  s.data.transport?.closed();
1204
1178
  connection.close();
1205
1179
  }
@@ -1213,14 +1187,171 @@ function transportOf(socket, settings, secure) {
1213
1187
  socket.data.transport = transport;
1214
1188
  return transport;
1215
1189
  }
1190
+ function listenerHandlers(settings, slots, handshaking, secure) {
1191
+ return {
1192
+ open(socket) {
1193
+ socket.data = { upgraded: false, handshaking: secure };
1194
+ 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?.();
1227
+ },
1228
+ close(socket) {
1229
+ handshaking.delete(socket);
1230
+ const connection = socket.data?.connection;
1231
+ if (connection)
1232
+ slots.release(connection);
1233
+ socket.data?.transport?.closed();
1234
+ connection?.close();
1235
+ }
1236
+ };
1237
+ }
1238
+
1239
+ // src/server/settings.ts
1240
+ var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
1241
+ var invalid = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
1242
+ function positive(name, value, fallback) {
1243
+ if (value === undefined)
1244
+ return fallback;
1245
+ if (!Number.isSafeInteger(value) || value < 1) {
1246
+ throw invalid(`${name} must be a positive integer, not ${value}`);
1247
+ }
1248
+ return value;
1249
+ }
1250
+ function timer(name, value, fallback) {
1251
+ const seconds = positive(name, value, fallback);
1252
+ if (seconds > MAX_TIMER_SECONDS) {
1253
+ throw invalid(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
1254
+ }
1255
+ return seconds;
1256
+ }
1257
+ function nonNegative(name, value, fallback) {
1258
+ if (value === undefined)
1259
+ return fallback;
1260
+ if (!Number.isFinite(value) || value < 0) {
1261
+ throw invalid(`${name} must be a number of seconds, 0 or more, not ${value}`);
1262
+ }
1263
+ return value;
1264
+ }
1265
+ function localLookup(local) {
1266
+ if (typeof local === "function")
1267
+ return local;
1268
+ if (Array.isArray(local) && local.every((domain) => typeof domain === "string")) {
1269
+ const domains = new Set(local.map((domain) => domain.toLowerCase()));
1270
+ return (domain) => domains.has(domain);
1271
+ }
1272
+ throw invalid("localDomains must be an array of domains or a function");
1273
+ }
1274
+ function settingsOf(options) {
1275
+ if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
1276
+ throw invalid(`"${options.hostname}" is not a host name`);
1277
+ }
1278
+ if (typeof options.onData !== "function") {
1279
+ throw invalid("onData must be a function: it is where messages go");
1280
+ }
1281
+ if (options.implicitTls && !options.tls) {
1282
+ throw invalid("implicitTls needs tls: { key, cert }");
1283
+ }
1284
+ if (options.mode === "submission" && !options.authenticate) {
1285
+ throw invalid("submission takes mail only from authenticated users, so it needs authenticate");
1286
+ }
1287
+ if (options.authenticate && !options.tls) {
1288
+ throw invalid("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1289
+ }
1290
+ const isLocal = localLookup(options.localDomains);
1291
+ const timeout = positive("timeout", options.timeout, 300);
1292
+ const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
1293
+ if (greetingDelay >= timeout) {
1294
+ throw invalid(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1295
+ }
1296
+ return {
1297
+ options,
1298
+ mode: options.mode ?? "mx",
1299
+ maxMessageSize: positive("maxMessageSize", options.maxMessageSize, 25 * 1024 * 1024),
1300
+ maxRecipients: positive("maxRecipients", options.maxRecipients, 100),
1301
+ maxConnections: positive("maxConnections", options.maxConnections, 1000),
1302
+ maxConnectionsPerClient: positive("maxConnectionsPerClient", options.maxConnectionsPerClient, 10),
1303
+ maxErrors: positive("maxErrors", options.maxErrors, 10),
1304
+ timeout,
1305
+ hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
1306
+ handshakeTimeout: positive("handshakeTimeout", options.handshakeTimeout, 10),
1307
+ greetingDelay,
1308
+ isLocal: (domain) => isLocal(domain.toLowerCase())
1309
+ };
1310
+ }
1311
+
1312
+ // src/server/slots.ts
1313
+ class Slots {
1314
+ #open = new Map;
1315
+ #perClient = new Map;
1316
+ get size() {
1317
+ return this.#open.size;
1318
+ }
1319
+ of(key) {
1320
+ return key === undefined ? 0 : this.#perClient.get(key) ?? 0;
1321
+ }
1322
+ take(connection, key) {
1323
+ this.#open.set(connection, key);
1324
+ if (key !== undefined)
1325
+ this.#perClient.set(key, this.of(key) + 1);
1326
+ }
1327
+ release(connection) {
1328
+ if (!this.#open.has(connection))
1329
+ return;
1330
+ const key = this.#open.get(connection);
1331
+ this.#open.delete(connection);
1332
+ if (key === undefined)
1333
+ return;
1334
+ const left = this.of(key) - 1;
1335
+ if (left > 0)
1336
+ this.#perClient.set(key, left);
1337
+ else
1338
+ this.#perClient.delete(key);
1339
+ }
1340
+ connections() {
1341
+ return [...this.#open.keys()];
1342
+ }
1343
+ }
1344
+
1345
+ // src/server/server.ts
1216
1346
  function createSmtpServer(options) {
1217
1347
  const settings = settingsOf(options);
1218
1348
  let listener;
1219
- const open = new Set;
1349
+ const slots = new Slots;
1350
+ const handshaking = new Set;
1220
1351
  const secure = options.implicitTls === true;
1221
1352
  return {
1222
1353
  get connections() {
1223
- return open.size;
1354
+ return slots.size;
1224
1355
  },
1225
1356
  async listen({ port, hostname = "0.0.0.0" }) {
1226
1357
  if (listener) {
@@ -1232,29 +1363,7 @@ function createSmtpServer(options) {
1232
1363
  ...secure && options.tls ? { tls: { key: options.tls.key, cert: options.tls.cert } } : {},
1233
1364
  socket: {
1234
1365
  ...handlers(settings),
1235
- open(socket) {
1236
- socket.data = { upgraded: false };
1237
- socket.timeout(settings.timeout);
1238
- if (open.size >= settings.maxConnections) {
1239
- const refused = new SocketTransport(socket, secure, () => {});
1240
- socket.data.transport = refused;
1241
- refused.write(`421 4.3.2 ${options.hostname} Too many connections, try later\r
1242
- `);
1243
- refused.end();
1244
- return;
1245
- }
1246
- const connection = new Connection(settings, transportOf(socket, settings, secure));
1247
- open.add(connection);
1248
- socket.data.connection = connection;
1249
- connection.open();
1250
- },
1251
- close(socket) {
1252
- const connection = socket.data.connection;
1253
- if (connection)
1254
- open.delete(connection);
1255
- socket.data.transport?.closed();
1256
- socket.data.connection?.close();
1257
- }
1366
+ ...listenerHandlers(settings, slots, handshaking, secure)
1258
1367
  }
1259
1368
  });
1260
1369
  return { port: listener.port, hostname: listener.hostname };
@@ -1264,14 +1373,23 @@ function createSmtpServer(options) {
1264
1373
  listener = undefined;
1265
1374
  if (!closeConnections)
1266
1375
  return;
1267
- for (const connection of [...open])
1268
- connection.close();
1376
+ const stuck = new Set;
1377
+ for (const socket of [...handshaking]) {
1378
+ stuck.add(socket.data?.connection);
1379
+ handshaking.delete(socket);
1380
+ socket.terminate();
1381
+ }
1382
+ for (const connection of slots.connections()) {
1383
+ if (!stuck.has(connection))
1384
+ connection.close();
1385
+ }
1269
1386
  }
1270
1387
  };
1271
1388
  }
1272
1389
  export {
1273
1390
  DataReader,
1274
1391
  SmtpError2 as SmtpError,
1392
+ clientKey,
1275
1393
  createSmtpServer,
1276
1394
  decodeLoginStep2 as decodeLoginStep,
1277
1395
  decodePlain2 as decodePlain,
@@ -1282,5 +1400,5 @@ export {
1282
1400
  reply
1283
1401
  };
1284
1402
 
1285
- //# debugId=04EE37708209636464756E2164756E21
1403
+ //# debugId=388406E838895A3964756E2164756E21
1286
1404
  //# sourceMappingURL=index.js.map