@bumail/smtp 0.1.0 → 0.2.1
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 +149 -7
- package/dist/chunks/index-3423jxf9.js +249 -0
- package/dist/chunks/index-3423jxf9.js.map +14 -0
- package/dist/client/content.d.ts +25 -0
- package/dist/client/content.d.ts.map +1 -0
- package/dist/client/inbox.d.ts +18 -0
- package/dist/client/inbox.d.ts.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +948 -0
- package/dist/client/index.js.map +22 -0
- package/dist/client/mx.d.ts +29 -0
- package/dist/client/mx.d.ts.map +1 -0
- package/dist/client/options.d.ts +136 -0
- package/dist/client/options.d.ts.map +1 -0
- package/dist/client/refusal.d.ts +15 -0
- package/dist/client/refusal.d.ts.map +1 -0
- package/dist/client/send.d.ts +13 -0
- package/dist/client/send.d.ts.map +1 -0
- package/dist/client/session.d.ts +11 -0
- package/dist/client/session.d.ts.map +1 -0
- package/dist/client/settings.d.ts +30 -0
- package/dist/client/settings.d.ts.map +1 -0
- package/dist/client/socket.d.ts +31 -0
- package/dist/client/socket.d.ts.map +1 -0
- package/dist/client/target.d.ts +29 -0
- package/dist/client/target.d.ts.map +1 -0
- package/dist/client/transaction.d.ts +15 -0
- package/dist/client/transaction.d.ts.map +1 -0
- package/dist/errors.d.ts +28 -4
- 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 +164 -197
- package/dist/index.js.map +9 -13
- package/dist/io/outgoing.d.ts +21 -0
- package/dist/io/outgoing.d.ts.map +1 -0
- package/dist/protocol/data-writer.d.ts +22 -0
- package/dist/protocol/data-writer.d.ts.map +1 -0
- package/dist/protocol/ehlo.d.ts +7 -0
- package/dist/protocol/ehlo.d.ts.map +1 -0
- package/dist/{server → protocol}/lines.d.ts +9 -4
- package/dist/protocol/lines.d.ts.map +1 -0
- package/dist/protocol/path.d.ts +24 -3
- package/dist/protocol/path.d.ts.map +1 -1
- package/dist/protocol/reply-reader.d.ts +19 -0
- package/dist/protocol/reply-reader.d.ts.map +1 -0
- package/dist/protocol/sasl.d.ts +4 -0
- package/dist/protocol/sasl.d.ts.map +1 -1
- package/dist/server/commands.d.ts.map +1 -1
- package/dist/server/connection.d.ts +8 -0
- package/dist/server/connection.d.ts.map +1 -1
- package/dist/server/input.d.ts.map +1 -1
- package/dist/server/options.d.ts +2 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/transport.d.ts +40 -4
- package/dist/server/transport.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide.md +407 -13
- package/docs/roadmap.md +43 -8
- package/docs/troubleshooting.md +706 -9
- package/package.json +12 -4
- package/dist/server/lines.d.ts.map +0 -1
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,uGAAuG;AACvG,MAAM,MAAM,aAAa,GACtB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,oBAAoB,GACpB,kBAAkB,GAClB,mBAAmB,GACnB,SAAS,GACT,WAAW,GACX,SAAS,GACT,oBAAoB,GACpB,iBAAiB,GACjB,YAAY,GACZ,kBAAkB,GAClB,mBAAmB,GACnB,SAAS,GACT,YAAY,CAAC;AAEhB,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACtB;AAED,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAYD;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,KAAK;IACnC,SAAkB,IAAI,eAAe;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;gBAG7C,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gBAAqB;CAU/B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { SmtpError, type SmtpErrorCode } from './errors';
|
|
2
2
|
export { type Command, type PathCommand, parseCommand, parsePathCommand, } from './protocol/command';
|
|
3
3
|
export { type DataChunk, DataReader } from './protocol/data';
|
|
4
|
-
export { type Path, parsePath } from './protocol/path';
|
|
4
|
+
export { type Path, parsePath, type SourceRoute } from './protocol/path';
|
|
5
5
|
export { formatReply, type Reply, reply } from './protocol/reply';
|
|
6
6
|
export { type Credentials, decodeLoginStep, decodePlain, } from './protocol/sasl';
|
|
7
7
|
export type { Envelope, HookResult, ReceivedMessage, Session, SmtpHooks, SmtpServerOptions, TlsOptions, } from './server/options';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,eAAe,EACf,WAAW,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACX,QAAQ,EACR,UAAU,EACV,eAAe,EACf,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,UAAU,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
// src/protocol/path.ts
|
|
11
|
-
var ATOM = /^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~\u0080-\u{10ffff}]+$/u;
|
|
12
|
-
var LABEL = /^(?:[A-Za-z0-9\u0080-\u{10ffff}](?:[A-Za-z0-9\-\u0080-\u{10ffff}]*[A-Za-z0-9\u0080-\u{10ffff}])?)$/u;
|
|
13
|
-
var INVISIBLE = /[\u0080-\u009f\u2028\u2029\p{Cf}]/u;
|
|
14
|
-
function isDotString(text) {
|
|
15
|
-
return text.split(".").every((atom) => ATOM.test(atom));
|
|
16
|
-
}
|
|
17
|
-
function isQuotedString(text) {
|
|
18
|
-
return /^"(?:[\x20\x21\x23-\x5b\x5d-\x7e\u0080-\u{10ffff}]|\\[\x20-\x7e])*"$/u.test(text);
|
|
19
|
-
}
|
|
20
|
-
function isDomain(domain) {
|
|
21
|
-
if (/^\[(?:IPv6:[0-9A-Fa-f:.]+|\d{1,3}(?:\.\d{1,3}){3})\]$/.test(domain))
|
|
22
|
-
return true;
|
|
23
|
-
return domain.length <= 255 && domain.split(".").every((label) => label.length <= 63 && LABEL.test(label));
|
|
24
|
-
}
|
|
25
|
-
function parsePath(text, allowNull) {
|
|
26
|
-
if (!text.startsWith("<") || !text.endsWith(">"))
|
|
27
|
-
return;
|
|
28
|
-
let inner = text.slice(1, -1);
|
|
29
|
-
if (INVISIBLE.test(inner))
|
|
30
|
-
return;
|
|
31
|
-
if (inner === "")
|
|
32
|
-
return allowNull ? { address: "", local: "", domain: "" } : undefined;
|
|
33
|
-
const route = /^@[^:]+:/.exec(inner);
|
|
34
|
-
if (route)
|
|
35
|
-
inner = inner.slice(route[0].length);
|
|
36
|
-
const at = inner.lastIndexOf("@");
|
|
37
|
-
if (at <= 0)
|
|
38
|
-
return;
|
|
39
|
-
const local = inner.slice(0, at);
|
|
40
|
-
const domain = inner.slice(at + 1);
|
|
41
|
-
if (local.length > 64 || !(isDotString(local) || isQuotedString(local)))
|
|
42
|
-
return;
|
|
43
|
-
if (!isDomain(domain))
|
|
44
|
-
return;
|
|
45
|
-
return {
|
|
46
|
-
address: `${local}@${domain.toLowerCase()}`,
|
|
47
|
-
local,
|
|
48
|
-
domain: domain.toLowerCase()
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
1
|
+
import {
|
|
2
|
+
SmtpError2,
|
|
3
|
+
isHelloName,
|
|
4
|
+
parsePath2,
|
|
5
|
+
decodePlain2,
|
|
6
|
+
decodeLoginStep2,
|
|
7
|
+
LineSplitter,
|
|
8
|
+
Outgoing
|
|
9
|
+
} from "./chunks/index-3423jxf9.js";
|
|
52
10
|
// src/protocol/command.ts
|
|
53
11
|
function parseCommand(line) {
|
|
54
12
|
const space = line.indexOf(" ");
|
|
@@ -63,7 +21,7 @@ function parsePathCommand(argument, keyword) {
|
|
|
63
21
|
const match = new RegExp(`^${keyword}:\\s*(<[^>]*>)(.*)$`, "i").exec(argument);
|
|
64
22
|
if (!match)
|
|
65
23
|
return;
|
|
66
|
-
const path =
|
|
24
|
+
const path = parsePath2(match[1], keyword === "FROM");
|
|
67
25
|
if (!path)
|
|
68
26
|
return;
|
|
69
27
|
const parameters = {};
|
|
@@ -165,36 +123,6 @@ function formatReply(reply, enhanced = true) {
|
|
|
165
123
|
}).join("");
|
|
166
124
|
}
|
|
167
125
|
var reply = (code, status, text) => status === undefined ? { code, text } : { code, status, text };
|
|
168
|
-
// src/protocol/sasl.ts
|
|
169
|
-
function base64ToText(text) {
|
|
170
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0)
|
|
171
|
-
return;
|
|
172
|
-
try {
|
|
173
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.fromBase64(text));
|
|
174
|
-
} catch {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function decodePlain(response) {
|
|
179
|
-
const text = base64ToText(response);
|
|
180
|
-
if (text === undefined)
|
|
181
|
-
return;
|
|
182
|
-
const parts = text.split("\x00");
|
|
183
|
-
if (parts.length !== 3)
|
|
184
|
-
return;
|
|
185
|
-
const [authorizationId = "", username = "", password = ""] = parts;
|
|
186
|
-
if (username === "" || password === "")
|
|
187
|
-
return;
|
|
188
|
-
return {
|
|
189
|
-
mechanism: "PLAIN",
|
|
190
|
-
username,
|
|
191
|
-
password,
|
|
192
|
-
...authorizationId === "" || authorizationId === username ? {} : { authorizationId }
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function decodeLoginStep(response) {
|
|
196
|
-
return base64ToText(response);
|
|
197
|
-
}
|
|
198
126
|
// src/server/guard.ts
|
|
199
127
|
var LOCAL_ERROR = reply(451, "4.3.0", "Local error in processing");
|
|
200
128
|
var TIMED_OUT = Symbol("timed out");
|
|
@@ -213,7 +141,7 @@ function timedOut(value) {
|
|
|
213
141
|
return value === TIMED_OUT;
|
|
214
142
|
}
|
|
215
143
|
function hookTimeout(name, seconds) {
|
|
216
|
-
return new
|
|
144
|
+
return new SmtpError2("HOOK_TIMEOUT", `${name} did not settle within hookTimeout (${seconds} s)`);
|
|
217
145
|
}
|
|
218
146
|
function refusalOf(name, answer, report) {
|
|
219
147
|
if (answer === undefined)
|
|
@@ -222,7 +150,7 @@ function refusalOf(name, answer, report) {
|
|
|
222
150
|
if (typeof code === "number" && code >= 400 && code < 600) {
|
|
223
151
|
return answer;
|
|
224
152
|
}
|
|
225
|
-
report(new
|
|
153
|
+
report(new SmtpError2("INVALID_HOOK_REPLY", `${name} answered ${JSON.stringify(answer)}, which is not a refusal: a hook refuses with a 4xx or 5xx reply, and accepts with undefined`));
|
|
226
154
|
return LOCAL_ERROR;
|
|
227
155
|
}
|
|
228
156
|
|
|
@@ -281,7 +209,7 @@ async function authenticate(connection, argument) {
|
|
|
281
209
|
state.waiting = "auth-plain";
|
|
282
210
|
return connection.send(reply(334, undefined, ""));
|
|
283
211
|
}
|
|
284
|
-
return check(connection,
|
|
212
|
+
return check(connection, decodePlain2(initial === "=" ? "" : initial));
|
|
285
213
|
case "LOGIN":
|
|
286
214
|
if (initial === undefined) {
|
|
287
215
|
state.waiting = "auth-login-user";
|
|
@@ -293,7 +221,7 @@ async function authenticate(connection, argument) {
|
|
|
293
221
|
}
|
|
294
222
|
}
|
|
295
223
|
function loginUser(connection, response) {
|
|
296
|
-
const user =
|
|
224
|
+
const user = decodeLoginStep2(response);
|
|
297
225
|
if (!user)
|
|
298
226
|
return check(connection, undefined);
|
|
299
227
|
connection.state.loginUser = user;
|
|
@@ -309,11 +237,11 @@ async function continueAuth(connection, line) {
|
|
|
309
237
|
}
|
|
310
238
|
switch (state.waiting) {
|
|
311
239
|
case "auth-plain":
|
|
312
|
-
return check(connection,
|
|
240
|
+
return check(connection, decodePlain2(line));
|
|
313
241
|
case "auth-login-user":
|
|
314
242
|
return loginUser(connection, line);
|
|
315
243
|
default: {
|
|
316
|
-
const password =
|
|
244
|
+
const password = decodeLoginStep2(line);
|
|
317
245
|
const username = state.loginUser ?? "";
|
|
318
246
|
return check(connection, password ? { mechanism: "LOGIN", username, password } : undefined);
|
|
319
247
|
}
|
|
@@ -433,7 +361,7 @@ function ehloLines(connection) {
|
|
|
433
361
|
return lines;
|
|
434
362
|
}
|
|
435
363
|
async function hello(connection, argument, esmtp) {
|
|
436
|
-
if (
|
|
364
|
+
if (!isHelloName(argument)) {
|
|
437
365
|
connection.fail(reply(501, "5.5.4", `Syntax: ${esmtp ? "EHLO" : "HELO"} hostname`));
|
|
438
366
|
return;
|
|
439
367
|
}
|
|
@@ -499,7 +427,7 @@ async function runCommand(connection, line) {
|
|
|
499
427
|
case "HELP":
|
|
500
428
|
return connection.send(reply(214, "2.0.0", "See RFC 5321"));
|
|
501
429
|
case "QUIT":
|
|
502
|
-
return connection.
|
|
430
|
+
return connection.quit(reply(221, "2.0.0", `${connection.settings.options.hostname} closing connection`));
|
|
503
431
|
case "STARTTLS":
|
|
504
432
|
return startTls(connection, argument);
|
|
505
433
|
case "AUTH":
|
|
@@ -565,22 +493,22 @@ function receivedField(connection, recipients, id, now = new Date) {
|
|
|
565
493
|
var TOO_BIG = reply(552, "5.3.4", "Message too big for system");
|
|
566
494
|
var BARE_LINE_BREAK = reply(550, "5.6.11", "Bare CR or LF is not allowed in a message");
|
|
567
495
|
function tooBig(maxMessageSize) {
|
|
568
|
-
return new
|
|
496
|
+
return new SmtpError2("MESSAGE_TOO_BIG", `The message is larger than maxMessageSize (${maxMessageSize} bytes); do not deliver it`);
|
|
569
497
|
}
|
|
570
498
|
function bareLineBreak() {
|
|
571
|
-
return new
|
|
499
|
+
return new SmtpError2("BARE_LINE_BREAK", "The message holds a bare CR or LF (SMTP smuggling); do not deliver it");
|
|
572
500
|
}
|
|
573
501
|
function connectionLost(ended) {
|
|
574
|
-
return new
|
|
502
|
+
return new SmtpError2("CONNECTION_LOST", ended ? "The connection closed before the reply to the message; do not deliver it" : "The connection closed before the end of the message; do not deliver it");
|
|
575
503
|
}
|
|
576
504
|
function notAnswered(seconds) {
|
|
577
|
-
return new
|
|
505
|
+
return new SmtpError2("HOOK_TIMEOUT", `onData did not answer within hookTimeout (${seconds} s); do not deliver it`);
|
|
578
506
|
}
|
|
579
507
|
function notReadInTime(seconds) {
|
|
580
|
-
return new
|
|
508
|
+
return new SmtpError2("HOOK_TIMEOUT", `onData did not read the message within hookTimeout (${seconds} s); do not deliver it`);
|
|
581
509
|
}
|
|
582
510
|
function notRead() {
|
|
583
|
-
return new
|
|
511
|
+
return new SmtpError2("MESSAGE_NOT_READ", "onData answered without reading the message to its end; it was not taken");
|
|
584
512
|
}
|
|
585
513
|
|
|
586
514
|
// src/server/intake.ts
|
|
@@ -755,61 +683,6 @@ class Intake {
|
|
|
755
683
|
}
|
|
756
684
|
}
|
|
757
685
|
|
|
758
|
-
// src/server/lines.ts
|
|
759
|
-
var MAX_LINE = 2048;
|
|
760
|
-
|
|
761
|
-
class LineSplitter {
|
|
762
|
-
#held = new Uint8Array(0);
|
|
763
|
-
#skipping = false;
|
|
764
|
-
#rest = new Uint8Array(0);
|
|
765
|
-
push(chunk) {
|
|
766
|
-
this.#rest = this.#held.length > 0 ? concat(this.#held, chunk) : chunk;
|
|
767
|
-
this.#held = new Uint8Array(0);
|
|
768
|
-
}
|
|
769
|
-
next() {
|
|
770
|
-
for (;; ) {
|
|
771
|
-
const data = this.#rest;
|
|
772
|
-
const lf = data.indexOf(10);
|
|
773
|
-
if (lf < 0) {
|
|
774
|
-
this.#rest = new Uint8Array(0);
|
|
775
|
-
if (this.#skipping)
|
|
776
|
-
return;
|
|
777
|
-
if (data.length > MAX_LINE) {
|
|
778
|
-
this.#skipping = true;
|
|
779
|
-
return { tooLong: true };
|
|
780
|
-
}
|
|
781
|
-
this.#held = data.slice();
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
const end = lf > 0 && data[lf - 1] === 13 ? lf - 1 : lf;
|
|
785
|
-
this.#rest = data.subarray(lf + 1);
|
|
786
|
-
if (this.#skipping) {
|
|
787
|
-
this.#skipping = false;
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
|
-
if (end > MAX_LINE)
|
|
791
|
-
return { tooLong: true };
|
|
792
|
-
return { line: new TextDecoder().decode(data.subarray(0, end)) };
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
takeRest() {
|
|
796
|
-
const rest = this.#rest;
|
|
797
|
-
this.#rest = new Uint8Array(0);
|
|
798
|
-
return rest;
|
|
799
|
-
}
|
|
800
|
-
clear() {
|
|
801
|
-
this.#held = new Uint8Array(0);
|
|
802
|
-
this.#rest = new Uint8Array(0);
|
|
803
|
-
this.#skipping = false;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
function concat(a, b) {
|
|
807
|
-
const out = new Uint8Array(a.length + b.length);
|
|
808
|
-
out.set(a, 0);
|
|
809
|
-
out.set(b, a.length);
|
|
810
|
-
return out;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
686
|
// src/server/input.ts
|
|
814
687
|
var INPUT_LIMIT = 64 * 1024;
|
|
815
688
|
|
|
@@ -1008,6 +881,14 @@ class Connection {
|
|
|
1008
881
|
this.send(answer);
|
|
1009
882
|
this.#closed = true;
|
|
1010
883
|
this.input.abort();
|
|
884
|
+
this.transport.abort();
|
|
885
|
+
}
|
|
886
|
+
quit(answer) {
|
|
887
|
+
if (this.#closed)
|
|
888
|
+
return;
|
|
889
|
+
this.send(answer);
|
|
890
|
+
this.#closed = true;
|
|
891
|
+
this.input.abort();
|
|
1011
892
|
this.transport.end();
|
|
1012
893
|
}
|
|
1013
894
|
fail(answer) {
|
|
@@ -1072,7 +953,8 @@ class Connection {
|
|
|
1072
953
|
}
|
|
1073
954
|
|
|
1074
955
|
// src/server/settings.ts
|
|
1075
|
-
var
|
|
956
|
+
var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
|
|
957
|
+
var invalid = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
|
|
1076
958
|
function positive(name, value, fallback) {
|
|
1077
959
|
if (value === undefined)
|
|
1078
960
|
return fallback;
|
|
@@ -1081,6 +963,13 @@ function positive(name, value, fallback) {
|
|
|
1081
963
|
}
|
|
1082
964
|
return value;
|
|
1083
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
|
+
}
|
|
1084
973
|
function nonNegative(name, value, fallback) {
|
|
1085
974
|
if (value === undefined)
|
|
1086
975
|
return fallback;
|
|
@@ -1128,72 +1017,137 @@ function settingsOf(options) {
|
|
|
1128
1017
|
maxConnections: positive("maxConnections", options.maxConnections, 1000),
|
|
1129
1018
|
maxErrors: positive("maxErrors", options.maxErrors, 10),
|
|
1130
1019
|
timeout,
|
|
1131
|
-
hookTimeout:
|
|
1020
|
+
hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
|
|
1132
1021
|
greetingDelay,
|
|
1133
1022
|
isLocal: (domain) => isLocal(domain.toLowerCase())
|
|
1134
1023
|
};
|
|
1135
1024
|
}
|
|
1136
1025
|
|
|
1137
1026
|
// src/server/transport.ts
|
|
1027
|
+
var CLOSE_GRACE_MS = 5000;
|
|
1028
|
+
var LINGER_QUIET_MS = 20;
|
|
1029
|
+
var LINGER_MAX_MS = 500;
|
|
1030
|
+
|
|
1138
1031
|
class SocketTransport {
|
|
1139
1032
|
#socket;
|
|
1140
1033
|
remoteAddress;
|
|
1141
1034
|
secure;
|
|
1142
1035
|
#startTls;
|
|
1143
|
-
#
|
|
1144
|
-
#waiters = [];
|
|
1036
|
+
#outgoing;
|
|
1145
1037
|
#ending = false;
|
|
1038
|
+
#closed = false;
|
|
1039
|
+
#paused = false;
|
|
1040
|
+
#grace;
|
|
1041
|
+
#linger;
|
|
1146
1042
|
constructor(socket, secure, startTls, remoteAddress = socket.remoteAddress) {
|
|
1147
1043
|
this.#socket = socket;
|
|
1148
1044
|
this.secure = secure;
|
|
1149
1045
|
this.#startTls = startTls;
|
|
1150
1046
|
this.remoteAddress = remoteAddress;
|
|
1047
|
+
this.#outgoing = new Outgoing((bytes) => socket.write(bytes));
|
|
1151
1048
|
}
|
|
1152
1049
|
write(text) {
|
|
1153
|
-
|
|
1154
|
-
if (this.#queue.length > 0) {
|
|
1155
|
-
this.#queue.push(bytes);
|
|
1050
|
+
if (this.#closed)
|
|
1156
1051
|
return;
|
|
1157
|
-
|
|
1158
|
-
const written = this.#socket.write(bytes);
|
|
1159
|
-
if (written < bytes.length)
|
|
1160
|
-
this.#queue.push(bytes.subarray(written));
|
|
1052
|
+
this.#outgoing.write(new TextEncoder().encode(text));
|
|
1161
1053
|
}
|
|
1162
1054
|
drain() {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const written = this.#socket.write(bytes);
|
|
1166
|
-
if (written < bytes.length) {
|
|
1167
|
-
this.#queue[0] = bytes.subarray(written);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
this.#queue.shift();
|
|
1171
|
-
}
|
|
1172
|
-
for (const wake of this.#waiters.splice(0))
|
|
1173
|
-
wake();
|
|
1174
|
-
if (this.#ending)
|
|
1175
|
-
this.#socket.end();
|
|
1055
|
+
if (this.#outgoing.drain() && this.#ending)
|
|
1056
|
+
this.#hangUp(true);
|
|
1176
1057
|
}
|
|
1177
1058
|
drained() {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1059
|
+
return this.#outgoing.drained();
|
|
1060
|
+
}
|
|
1061
|
+
received() {
|
|
1062
|
+
const linger = this.#linger;
|
|
1063
|
+
if (!linger)
|
|
1064
|
+
return;
|
|
1065
|
+
const left = linger.until - Date.now();
|
|
1066
|
+
if (left <= 0)
|
|
1067
|
+
this.#terminate();
|
|
1068
|
+
else
|
|
1069
|
+
this.#lingerFor(Math.min(LINGER_QUIET_MS, left));
|
|
1181
1070
|
}
|
|
1182
1071
|
closed() {
|
|
1183
|
-
this.#
|
|
1184
|
-
|
|
1185
|
-
|
|
1072
|
+
this.#closed = true;
|
|
1073
|
+
this.#disarm();
|
|
1074
|
+
this.#stopLingering();
|
|
1075
|
+
this.#outgoing.clear();
|
|
1186
1076
|
}
|
|
1187
1077
|
end() {
|
|
1188
|
-
if (this.#
|
|
1189
|
-
|
|
1078
|
+
if (this.#closed)
|
|
1079
|
+
return;
|
|
1080
|
+
this.#arm();
|
|
1081
|
+
if (this.#outgoing.empty)
|
|
1082
|
+
this.#hangUp(false);
|
|
1190
1083
|
else
|
|
1191
1084
|
this.#ending = true;
|
|
1192
1085
|
}
|
|
1086
|
+
abort() {
|
|
1087
|
+
if (this.#closed)
|
|
1088
|
+
return;
|
|
1089
|
+
if (this.#outgoing.empty)
|
|
1090
|
+
this.end();
|
|
1091
|
+
else
|
|
1092
|
+
this.#terminate();
|
|
1093
|
+
}
|
|
1094
|
+
#hangUp(drained) {
|
|
1095
|
+
this.#ending = false;
|
|
1096
|
+
if (this.#linger)
|
|
1097
|
+
return;
|
|
1098
|
+
if (!this.#paused) {
|
|
1099
|
+
this.#shutdown(drained);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
this.resume();
|
|
1103
|
+
this.#linger = { drained, until: Date.now() + LINGER_MAX_MS };
|
|
1104
|
+
this.#lingerFor(LINGER_QUIET_MS);
|
|
1105
|
+
}
|
|
1106
|
+
#lingerFor(ms) {
|
|
1107
|
+
const linger = this.#linger;
|
|
1108
|
+
if (!linger)
|
|
1109
|
+
return;
|
|
1110
|
+
clearTimeout(linger.timer);
|
|
1111
|
+
linger.timer = setTimeout(() => {
|
|
1112
|
+
if (ms < LINGER_QUIET_MS)
|
|
1113
|
+
return this.#terminate();
|
|
1114
|
+
this.#stopLingering();
|
|
1115
|
+
this.#shutdown(linger.drained);
|
|
1116
|
+
}, ms);
|
|
1117
|
+
linger.timer.unref?.();
|
|
1118
|
+
}
|
|
1119
|
+
#stopLingering() {
|
|
1120
|
+
clearTimeout(this.#linger?.timer);
|
|
1121
|
+
this.#linger = undefined;
|
|
1122
|
+
}
|
|
1123
|
+
#shutdown(drained) {
|
|
1124
|
+
if (drained && this.secure)
|
|
1125
|
+
this.#socket.shutdown();
|
|
1126
|
+
else
|
|
1127
|
+
this.#socket.shutdown(true);
|
|
1128
|
+
}
|
|
1129
|
+
#terminate() {
|
|
1130
|
+
this.#disarm();
|
|
1131
|
+
this.#stopLingering();
|
|
1132
|
+
this.#outgoing.clear();
|
|
1133
|
+
this.#socket.terminate();
|
|
1134
|
+
}
|
|
1135
|
+
#arm() {
|
|
1136
|
+
if (this.#grace)
|
|
1137
|
+
return;
|
|
1138
|
+
this.#grace = setTimeout(() => this.#terminate(), CLOSE_GRACE_MS);
|
|
1139
|
+
this.#grace.unref?.();
|
|
1140
|
+
}
|
|
1141
|
+
#disarm() {
|
|
1142
|
+
clearTimeout(this.#grace);
|
|
1143
|
+
this.#grace = undefined;
|
|
1144
|
+
}
|
|
1193
1145
|
pause() {
|
|
1146
|
+
this.#paused = true;
|
|
1194
1147
|
this.#socket.pause();
|
|
1195
1148
|
}
|
|
1196
1149
|
resume() {
|
|
1150
|
+
this.#paused = false;
|
|
1197
1151
|
this.#socket.resume();
|
|
1198
1152
|
}
|
|
1199
1153
|
startTls() {
|
|
@@ -1211,6 +1165,7 @@ function handlers(settings) {
|
|
|
1211
1165
|
data(socket, chunk) {
|
|
1212
1166
|
if (socket.data.upgraded)
|
|
1213
1167
|
return;
|
|
1168
|
+
socket.data.transport?.received();
|
|
1214
1169
|
socket.timeout(settings.timeout);
|
|
1215
1170
|
socket.data.connection?.receive(chunk);
|
|
1216
1171
|
},
|
|
@@ -1224,7 +1179,11 @@ function handlers(settings) {
|
|
|
1224
1179
|
timeout(socket) {
|
|
1225
1180
|
if (socket.data.upgraded)
|
|
1226
1181
|
return;
|
|
1227
|
-
socket.data
|
|
1182
|
+
const { connection, transport } = socket.data;
|
|
1183
|
+
if (connection?.closed)
|
|
1184
|
+
transport?.abort();
|
|
1185
|
+
else
|
|
1186
|
+
connection?.close(idle);
|
|
1228
1187
|
}
|
|
1229
1188
|
};
|
|
1230
1189
|
}
|
|
@@ -1257,15 +1216,15 @@ function transportOf(socket, settings, secure) {
|
|
|
1257
1216
|
function createSmtpServer(options) {
|
|
1258
1217
|
const settings = settingsOf(options);
|
|
1259
1218
|
let listener;
|
|
1260
|
-
|
|
1219
|
+
const open = new Set;
|
|
1261
1220
|
const secure = options.implicitTls === true;
|
|
1262
1221
|
return {
|
|
1263
1222
|
get connections() {
|
|
1264
|
-
return open;
|
|
1223
|
+
return open.size;
|
|
1265
1224
|
},
|
|
1266
1225
|
async listen({ port, hostname = "0.0.0.0" }) {
|
|
1267
1226
|
if (listener) {
|
|
1268
|
-
throw new
|
|
1227
|
+
throw new SmtpError2("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
|
|
1269
1228
|
}
|
|
1270
1229
|
listener = Bun.listen({
|
|
1271
1230
|
hostname,
|
|
@@ -1276,19 +1235,23 @@ function createSmtpServer(options) {
|
|
|
1276
1235
|
open(socket) {
|
|
1277
1236
|
socket.data = { upgraded: false };
|
|
1278
1237
|
socket.timeout(settings.timeout);
|
|
1279
|
-
if (open >= settings.maxConnections) {
|
|
1280
|
-
|
|
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
|
|
1281
1242
|
`);
|
|
1243
|
+
refused.end();
|
|
1282
1244
|
return;
|
|
1283
1245
|
}
|
|
1284
|
-
open++;
|
|
1285
1246
|
const connection = new Connection(settings, transportOf(socket, settings, secure));
|
|
1247
|
+
open.add(connection);
|
|
1286
1248
|
socket.data.connection = connection;
|
|
1287
1249
|
connection.open();
|
|
1288
1250
|
},
|
|
1289
1251
|
close(socket) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1252
|
+
const connection = socket.data.connection;
|
|
1253
|
+
if (connection)
|
|
1254
|
+
open.delete(connection);
|
|
1292
1255
|
socket.data.transport?.closed();
|
|
1293
1256
|
socket.data.connection?.close();
|
|
1294
1257
|
}
|
|
@@ -1299,21 +1262,25 @@ function createSmtpServer(options) {
|
|
|
1299
1262
|
stop(closeConnections = false) {
|
|
1300
1263
|
listener?.stop(closeConnections);
|
|
1301
1264
|
listener = undefined;
|
|
1265
|
+
if (!closeConnections)
|
|
1266
|
+
return;
|
|
1267
|
+
for (const connection of [...open])
|
|
1268
|
+
connection.close();
|
|
1302
1269
|
}
|
|
1303
1270
|
};
|
|
1304
1271
|
}
|
|
1305
1272
|
export {
|
|
1306
1273
|
DataReader,
|
|
1307
|
-
SmtpError,
|
|
1274
|
+
SmtpError2 as SmtpError,
|
|
1308
1275
|
createSmtpServer,
|
|
1309
|
-
decodeLoginStep,
|
|
1310
|
-
decodePlain,
|
|
1276
|
+
decodeLoginStep2 as decodeLoginStep,
|
|
1277
|
+
decodePlain2 as decodePlain,
|
|
1311
1278
|
formatReply,
|
|
1312
1279
|
parseCommand,
|
|
1313
|
-
parsePath,
|
|
1280
|
+
parsePath2 as parsePath,
|
|
1314
1281
|
parsePathCommand,
|
|
1315
1282
|
reply
|
|
1316
1283
|
};
|
|
1317
1284
|
|
|
1318
|
-
//# debugId=
|
|
1285
|
+
//# debugId=04EE37708209636464756E2164756E21
|
|
1319
1286
|
//# sourceMappingURL=index.js.map
|