@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.
Files changed (65) hide show
  1. package/README.md +149 -7
  2. package/dist/chunks/index-3423jxf9.js +249 -0
  3. package/dist/chunks/index-3423jxf9.js.map +14 -0
  4. package/dist/client/content.d.ts +25 -0
  5. package/dist/client/content.d.ts.map +1 -0
  6. package/dist/client/inbox.d.ts +18 -0
  7. package/dist/client/inbox.d.ts.map +1 -0
  8. package/dist/client/index.d.ts +6 -0
  9. package/dist/client/index.d.ts.map +1 -0
  10. package/dist/client/index.js +948 -0
  11. package/dist/client/index.js.map +22 -0
  12. package/dist/client/mx.d.ts +29 -0
  13. package/dist/client/mx.d.ts.map +1 -0
  14. package/dist/client/options.d.ts +136 -0
  15. package/dist/client/options.d.ts.map +1 -0
  16. package/dist/client/refusal.d.ts +15 -0
  17. package/dist/client/refusal.d.ts.map +1 -0
  18. package/dist/client/send.d.ts +13 -0
  19. package/dist/client/send.d.ts.map +1 -0
  20. package/dist/client/session.d.ts +11 -0
  21. package/dist/client/session.d.ts.map +1 -0
  22. package/dist/client/settings.d.ts +30 -0
  23. package/dist/client/settings.d.ts.map +1 -0
  24. package/dist/client/socket.d.ts +31 -0
  25. package/dist/client/socket.d.ts.map +1 -0
  26. package/dist/client/target.d.ts +29 -0
  27. package/dist/client/target.d.ts.map +1 -0
  28. package/dist/client/transaction.d.ts +15 -0
  29. package/dist/client/transaction.d.ts.map +1 -0
  30. package/dist/errors.d.ts +28 -4
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +164 -197
  35. package/dist/index.js.map +9 -13
  36. package/dist/io/outgoing.d.ts +21 -0
  37. package/dist/io/outgoing.d.ts.map +1 -0
  38. package/dist/protocol/data-writer.d.ts +22 -0
  39. package/dist/protocol/data-writer.d.ts.map +1 -0
  40. package/dist/protocol/ehlo.d.ts +7 -0
  41. package/dist/protocol/ehlo.d.ts.map +1 -0
  42. package/dist/{server → protocol}/lines.d.ts +9 -4
  43. package/dist/protocol/lines.d.ts.map +1 -0
  44. package/dist/protocol/path.d.ts +24 -3
  45. package/dist/protocol/path.d.ts.map +1 -1
  46. package/dist/protocol/reply-reader.d.ts +19 -0
  47. package/dist/protocol/reply-reader.d.ts.map +1 -0
  48. package/dist/protocol/sasl.d.ts +4 -0
  49. package/dist/protocol/sasl.d.ts.map +1 -1
  50. package/dist/server/commands.d.ts.map +1 -1
  51. package/dist/server/connection.d.ts +8 -0
  52. package/dist/server/connection.d.ts.map +1 -1
  53. package/dist/server/input.d.ts.map +1 -1
  54. package/dist/server/options.d.ts +2 -1
  55. package/dist/server/options.d.ts.map +1 -1
  56. package/dist/server/server.d.ts.map +1 -1
  57. package/dist/server/settings.d.ts.map +1 -1
  58. package/dist/server/transport.d.ts +40 -4
  59. package/dist/server/transport.d.ts.map +1 -1
  60. package/docs/README.md +3 -3
  61. package/docs/guide.md +407 -13
  62. package/docs/roadmap.md +43 -8
  63. package/docs/troubleshooting.md +706 -9
  64. package/package.json +12 -4
  65. package/dist/server/lines.d.ts.map +0 -1
package/README.md CHANGED
@@ -4,7 +4,10 @@ An SMTP server for Bun, on `Bun.listen`: RFC 5321 with PIPELINING, SIZE,
4
4
  8BITMIME, SMTPUTF8 and ENHANCEDSTATUSCODES; STARTTLS or implicit TLS; AUTH
5
5
  PLAIN and LOGIN, only once encrypted; hooks where your app accepts or
6
6
  refuses a connection, a sender, a recipient or a message. It is never an
7
- open relay. No dependency; `typescript` is an optional peer, for the types.
7
+ open relay. And, on `@bumail/smtp/client`, a client that delivers one
8
+ message to a smarthost, a submission server or a domain's MX hosts. No
9
+ dependency; `typescript` is an optional peer, for the types. MX delivery
10
+ needs a resolver: install `@bumail/dns` for one.
8
11
 
9
12
  ```sh
10
13
  bun add @bumail/smtp
@@ -165,6 +168,100 @@ console.log(`listening on ${port}, ${server.connections} open`);
165
168
  // later: server.stop(true) hangs up on every client too
166
169
  ```
167
170
 
171
+ ## Send mail: to a smarthost, a submission server or Mailpit
172
+
173
+ `sendMail(message, options)` from `@bumail/smtp/client` delivers one
174
+ message to one host. The message is the whole RFC 5322 text — a string,
175
+ bytes, or a `ReadableStream` — with lines ending in CRLF.
176
+
177
+ ```ts
178
+ import { sendMail } from '@bumail/smtp/client';
179
+
180
+ const message = [
181
+ 'From: Alice <alice@example.com>',
182
+ 'To: <bob@example.org>',
183
+ 'Subject: Hello',
184
+ `Date: ${new Date().toUTCString()}`,
185
+ '',
186
+ 'Hi Bob.',
187
+ '',
188
+ ].join('\r\n');
189
+
190
+ // Submission on 587: STARTTLS, the certificate checked, then AUTH.
191
+ const result = await sendMail(message, {
192
+ host: 'smtp.example.com',
193
+ port: 587,
194
+ from: 'alice@example.com',
195
+ to: ['bob@example.org'],
196
+ auth: { username: 'alice', password: Bun.env['SMTP_PASSWORD'] ?? '' },
197
+ });
198
+ console.log(result.reply.code, result.accepted, result.rejected);
199
+
200
+ // Implicit TLS on 465.
201
+ await sendMail(message, { host: 'smtp.example.com', secure: true, from: 'alice@example.com', to: 'bob@example.org', auth: { username: 'alice', password: 'secret' } });
202
+
203
+ // Mailpit on 1025 (docker run -p 8025:8025 -p 1025:1025 axllent/mailpit): no TLS, no AUTH.
204
+ await sendMail(message, { host: 'localhost', port: 1025, from: 'alice@example.com', to: 'bob@example.org' });
205
+ ```
206
+
207
+ It resolves once the server took the message for one recipient at least:
208
+ `accepted` and `rejected` give each recipient with the server's reply,
209
+ `reply` the reply to the final dot, and `tls` whether the session was
210
+ encrypted and the certificate checked. Anything else rejects with an
211
+ `SmtpError` whose `temporary` says whether trying later may succeed.
212
+
213
+ ## Send mail: direct to a domain's MX
214
+
215
+ `{ domain, resolver }` instead of `{ host }` looks up the domain's MX
216
+ records through a `Resolver` from `@bumail/dns`, and tries its hosts by
217
+ preference (RFC 5321 §5.1), the domain's own address when it has none.
218
+ TLS is opportunistic: STARTTLS when offered, the certificate not checked.
219
+ `helo` is required: your server's public name, the one its address
220
+ resolves back to.
221
+
222
+ `@bumail/dns` must be installed for this — or pass any object with its
223
+ `mx`, `a` and `aaaa` methods:
224
+
225
+ ```sh
226
+ bun add @bumail/smtp @bumail/dns
227
+ ```
228
+
229
+ ```ts
230
+ import { cachedResolver, nodeResolver } from '@bumail/dns';
231
+ import { SmtpError, sendMail } from '@bumail/smtp/client';
232
+
233
+ const resolver = cachedResolver(nodeResolver());
234
+ const message = [
235
+ 'From: Alice <alice@example.com>',
236
+ 'To: <bob@example.org>, <carol@example.org>',
237
+ 'Subject: Hello',
238
+ `Date: ${new Date().toUTCString()}`,
239
+ '',
240
+ 'Hi both.',
241
+ '',
242
+ ].join('\r\n');
243
+
244
+ try {
245
+ const result = await sendMail(message, {
246
+ domain: 'example.org',
247
+ resolver,
248
+ from: 'alice@example.com',
249
+ to: ['bob@example.org', 'carol@example.org'],
250
+ helo: 'mail.example.com',
251
+ });
252
+ console.log(`taken by ${result.host}`, result.tls);
253
+ for (const { recipient, reply } of result.rejected) {
254
+ console.log(recipient, reply.code >= 500 ? 'bounce' : 'try again later');
255
+ }
256
+ } catch (error) {
257
+ if (!(error instanceof SmtpError)) throw error;
258
+ console.log(error.code, error.temporary ? 'try again later' : 'bounce', error.message);
259
+ }
260
+ ```
261
+
262
+ A connection that fails, or a 4xx before MAIL FROM, moves on to the next
263
+ host; a 5xx stops. A null MX (RFC 7505) fails at once with `NULL_MX`.
264
+
168
265
  ## Security
169
266
 
170
267
  - **Never an open relay.** A recipient outside `localDomains` is refused
@@ -191,10 +288,39 @@ console.log(`listening on ${port}, ${server.connections} open`);
191
288
  from `onConnect` goes out at once, delay or not.
192
289
  - **Bounded memory.** The server holds 64 KiB of a client's input and of a
193
290
  message at most, and stops reading the client past that; replies to a
194
- client that reads slowly wait in the server, none is lost.
291
+ client that reads slowly wait in the server, and none is lost while the
292
+ connection is open. A client that stops reading altogether is hung up on
293
+ at its `timeout`, what it never read dropped, so it cannot hold a slot of
294
+ `maxConnections`, on TLS too.
195
295
 
196
296
  ## Traps
197
297
 
298
+ **Client**
299
+
300
+ - **Opportunistic TLS does not check the certificate.** It is the default
301
+ for MX delivery, as RFC 7435 has it: it beats a passive eavesdropper,
302
+ not an active attacker. `result.tls.verified` says whether the
303
+ certificate checked out anyway; pass `tls: 'required'` to refuse a host
304
+ whose certificate does not. With `auth` or `secure`, `required` is the
305
+ default.
306
+ - **`sendMail` is not a queue.** It tries one destination once — every MX
307
+ host of it, in turn — and gives up. Retrying a `temporary` failure later,
308
+ and bouncing a permanent one, is the caller's job until `@bumail/queue`.
309
+ - **Credentials go only to a checked certificate.** `auth` with `tls:
310
+ 'none'` or `'opportunistic'` is refused with `INVALID_OPTION` before
311
+ connecting; leave `tls` out and it is `'required'`.
312
+ `allowPlaintextAuth: true` lifts that, for a local test server only.
313
+ - **By MX, `helo` is required.** The machine's own name (`laptop.local`)
314
+ is rarely one that resolves back to your address, which receiving hosts
315
+ penalise; to a `{ host }` it stays the default.
316
+ - **A bare CR or LF in the message is refused** with `BARE_LINE_BREAK`, as
317
+ a server must refuse it (SMTP smuggling). End every line with CRLF, or
318
+ pass `normalizeLineEnds: true`.
319
+ - **One destination per call.** By MX, every recipient should be at
320
+ `domain`; group them by domain first.
321
+
322
+ **Server**
323
+
198
324
  - **Read `message.content` to its end before keeping anything.** An
199
325
  `onData` that answers before the end gets the client `451 4.3.0`, never
200
326
  `250`, and `onError` an `SmtpError` (`MESSAGE_NOT_READ`). The stream ends
@@ -236,9 +362,9 @@ console.log(`listening on ${port}, ${server.connections} open`);
236
362
  | export | |
237
363
  | --- | --- |
238
364
  | `createSmtpServer(options)` | the server; throws an `SmtpError` (`INVALID_OPTION`) on a bad option |
239
- | `SmtpServer` | `listen({ port, hostname? })` (once; again throws `ALREADY_LISTENING`), `stop(closeConnections?)`, `connections` |
365
+ | `SmtpServer` | `listen({ port, hostname? })` (once; again throws `ALREADY_LISTENING`), `stop(closeConnections?)` (`true` hangs up on every client, after STARTTLS too), `connections` |
240
366
  | `SmtpServerOptions` | `hostname`, `mode`, `localDomains`, `tls`, `implicitTls`, `authenticate`, the limits, `hookTimeout`, `greetingDelay`, `onError`, and the hooks |
241
- | `SmtpError`, `SmtpErrorCode` | `code`: `INVALID_OPTION`, `ALREADY_LISTENING`, and what a content stream or `onError` can get: `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK`, `CONNECTION_LOST`, `HOOK_TIMEOUT`, `INVALID_HOOK_REPLY`, `MESSAGE_NOT_READ` |
367
+ | `SmtpError`, `SmtpErrorCode` | `code`: `INVALID_OPTION`, `ALREADY_LISTENING`, and what a content stream or `onError` can get: `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK`, `CONNECTION_LOST`, `HOOK_TIMEOUT`, `INVALID_HOOK_REPLY`, `MESSAGE_NOT_READ`; `sendMail`'s are listed below. `temporary`, `reply` and `rejected` are set for `sendMail`'s errors |
242
368
  | `SmtpHooks` | `onConnect`, `onMailFrom`, `onRcptTo`, `onData` |
243
369
  | `HookResult` | what a hook returns: `undefined` to accept, a `Reply` to refuse |
244
370
  | `Session` | `id`, `remoteAddress`, `secure`, `helo`, `esmtp`, `user`, and `data` for your own state |
@@ -247,17 +373,33 @@ console.log(`listening on ${port}, ${server.connections} open`);
247
373
  | `Credentials` | what `authenticate` receives: `mechanism`, `username`, `password`, `authorizationId?` |
248
374
  | `reply(code, status, text)`, `Reply` | a reply, for a hook to refuse with |
249
375
  | `formatReply(reply, enhanced?)` | a reply as sent on the wire, CRLF included |
250
- | `parsePath(text, allowNull)`, `Path` | `<local@domain>` (RFC 5321 §4.1.2) to `{ address, local, domain }` |
376
+ | `parsePath(text, allowNull, sourceRoute?)`, `Path`, `SourceRoute` | `<local@domain>` (RFC 5321 §4.1.2) to `{ address, local, domain }`; a source route `@a,@b:` is dropped (`'discard'`, the default) or refused (`'refuse'`); a control character, `>` or a lone surrogate anywhere, or an IPv4 literal octet above 255, refuses the path |
251
377
  | `parseCommand(line)`, `Command` | a command line to `{ verb, argument }` |
252
378
  | `parsePathCommand(argument, 'FROM' \| 'TO')`, `PathCommand` | the argument of MAIL or RCPT to `{ path, parameters }` |
253
379
  | `DataReader`, `DataChunk` | reads DATA: dot-unstuffing, the terminator, bare CR and LF counted |
254
380
  | `decodePlain(response)`, `decodeLoginStep(response)` | SASL PLAIN (RFC 4616) and LOGIN responses to `Credentials` or text |
255
381
 
382
+ ### `@bumail/smtp/client`
383
+
384
+ | export | |
385
+ | --- | --- |
386
+ | `sendMail(message, options)` | delivers one message; resolves to a `SendMailResult`, rejects with an `SmtpError` |
387
+ | `SendMailOptions` | `from`, `to`, and `{ host, port? }` or `{ domain, resolver, port? }`; `helo`, `tls`, `secure`, `ca`, `auth`, `allowPlaintextAuth`, `size`, `smtputf8`, `normalizeLineEnds`, `timeouts`, `deadline` |
388
+ | `HostDestination`, `MxDestination`, `SendMailEnvelope` | the parts of `SendMailOptions` |
389
+ | `SendMailResult` | `accepted`, `rejected`, `reply`, `host`, `port`, `tls` (`false` or `{ verified }`), `authenticated` |
390
+ | `SendMailAuth` | `username`, `password`, `mechanism?` (`PLAIN` or `LOGIN`) |
391
+ | `SendMailTimeouts` | seconds: `connect`, `greeting`, `command`, `mail`, `rcpt`, `dataStart`, `dataBlock`, `dataEnd` |
392
+ | `TlsMode` | `'opportunistic'`, `'required'` or `'none'` |
393
+ | `MessageSource` | `Uint8Array`, `string` or `ReadableStream<Uint8Array>` |
394
+ | `MxResolver` | what MX delivery asks of the DNS, by shape: `mx`, `a` and `aaaa`, as `@bumail/dns`'s `Resolver` has them |
395
+ | `resolveMx(domain, resolver)`, `MailHost` | a domain's mail hosts in the order to try them: `{ host, priority, implicit }`; `NULL_MX` or `DNS_FAILED` otherwise |
396
+ | `SmtpError`, `SmtpErrorCode`, `SmtpErrorDetails`, `RecipientReply`, `Reply` | as above; `sendMail` adds the codes `CONNECTION_FAILED`, `CONNECTION_LOST`, `TIMEOUT`, `BAD_REPLY`, `REFUSED`, `RECIPIENTS_REFUSED`, `TLS_UNAVAILABLE`, `TLS_FAILED`, `AUTH_UNAVAILABLE`, `EXTENSION_MISSING`, `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK`, `NULL_MX`, `DNS_FAILED`, `INVALID_OPTION` |
397
+
256
398
  ## Documentation
257
399
 
258
400
  These pages ship in the package, under `docs/`.
259
401
 
260
402
  - [Index](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/README.md): the pages, and when to read each.
261
- - [Guide](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/guide.md): the session and every reply, hooks and their order, `Session.data`, what `onData` receives, TLS, the RFCs implemented and what is not.
262
- - [Troubleshooting](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/troubleshooting.md): every error, and the replies a client reports.
403
+ - [Guide](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/guide.md): the session and every reply, hooks and their order, `Session.data`, what `onData` receives, TLS, sending mail with the client and trying it with Mailpit, the RFCs implemented and what is not.
404
+ - [Troubleshooting](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/troubleshooting.md): every error, the replies a client reports, and every error `sendMail` rejects with.
263
405
  - [Roadmap](https://github.com/softistx/bumail/blob/develop/packages/smtp/docs/roadmap.md): what is coming, and what is not planned.
@@ -0,0 +1,249 @@
1
+ // src/errors.ts
2
+ var TEMPORARY = new Set([
3
+ "CONNECTION_LOST",
4
+ "CONNECTION_FAILED",
5
+ "TIMEOUT",
6
+ "BAD_REPLY",
7
+ "TLS_UNAVAILABLE",
8
+ "TLS_FAILED"
9
+ ]);
10
+
11
+ class SmtpError2 extends Error {
12
+ name = "SmtpError";
13
+ code;
14
+ temporary;
15
+ reply;
16
+ rejected;
17
+ constructor(code, message, details = {}) {
18
+ super(message);
19
+ this.code = code;
20
+ this.temporary = details.temporary ?? (details.reply ? details.reply.code < 500 : TEMPORARY.has(code));
21
+ if (details.reply)
22
+ this.reply = details.reply;
23
+ if (details.rejected)
24
+ this.rejected = details.rejected;
25
+ }
26
+ }
27
+
28
+ // src/protocol/path.ts
29
+ var ATOM = /^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~\u0080-\u{10ffff}]+$/u;
30
+ var LABEL = /^(?:[A-Za-z0-9\u0080-\u{10ffff}](?:[A-Za-z0-9\-\u0080-\u{10ffff}]*[A-Za-z0-9\u0080-\u{10ffff}])?)$/u;
31
+ var INVISIBLE = /[\u0080-\u009f\u2028\u2029\p{Cf}\p{Cs}]/u;
32
+ function hasControl(text) {
33
+ for (let i = 0;i < text.length; i++) {
34
+ const code = text.charCodeAt(i);
35
+ if (code < 32 || code === 127 || code === 62)
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ var HOP = /^@(\[[^\]]*\]|[^,:@[\]]*)([,:])/;
41
+ function isDotString(text) {
42
+ return text.split(".").every((atom) => ATOM.test(atom));
43
+ }
44
+ function isQuotedString(text) {
45
+ return /^"(?:[\x20\x21\x23-\x5b\x5d-\x7e\u0080-\u{10ffff}]|\\[\x20-\x7e])*"$/u.test(text);
46
+ }
47
+ function isAddressLiteral(text) {
48
+ if (/^\[IPv6:[0-9a-f:.]+\]$/i.test(text))
49
+ return true;
50
+ const ipv4 = /^\[(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\]$/.exec(text);
51
+ return ipv4?.slice(1).every((octet) => Number(octet) <= 255) === true;
52
+ }
53
+ function isHelloName(text) {
54
+ return /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/.test(text) || isAddressLiteral(text);
55
+ }
56
+ function isDomain(domain) {
57
+ if (isAddressLiteral(domain))
58
+ return true;
59
+ return domain.length <= 255 && domain.split(".").every((label) => label.length <= 63 && LABEL.test(label));
60
+ }
61
+ function afterRoute(inner) {
62
+ let rest = inner;
63
+ for (;; ) {
64
+ const hop = HOP.exec(rest);
65
+ if (!hop || !isDomain(hop[1]))
66
+ return;
67
+ rest = rest.slice(hop[0].length);
68
+ if (hop[2] === ":")
69
+ return rest;
70
+ }
71
+ }
72
+ function parsePath2(text, allowNull, sourceRoute = "discard") {
73
+ if (!text.startsWith("<") || !text.endsWith(">"))
74
+ return;
75
+ let inner = text.slice(1, -1);
76
+ if (hasControl(inner) || INVISIBLE.test(inner))
77
+ return;
78
+ if (inner === "")
79
+ return allowNull ? { address: "", local: "", domain: "" } : undefined;
80
+ if (inner.startsWith("@")) {
81
+ if (sourceRoute === "refuse")
82
+ return;
83
+ inner = afterRoute(inner);
84
+ if (inner === undefined)
85
+ return;
86
+ }
87
+ const at = inner.lastIndexOf("@");
88
+ if (at <= 0)
89
+ return;
90
+ const local = inner.slice(0, at);
91
+ const domain = inner.slice(at + 1);
92
+ if (local.length > 64 || !(isDotString(local) || isQuotedString(local)))
93
+ return;
94
+ if (!isDomain(domain))
95
+ return;
96
+ return {
97
+ address: `${local}@${domain.toLowerCase()}`,
98
+ local,
99
+ domain: domain.toLowerCase()
100
+ };
101
+ }
102
+
103
+ // src/protocol/sasl.ts
104
+ function base64ToText(text) {
105
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0)
106
+ return;
107
+ try {
108
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.fromBase64(text));
109
+ } catch {
110
+ return;
111
+ }
112
+ }
113
+ function decodePlain2(response) {
114
+ const text = base64ToText(response);
115
+ if (text === undefined)
116
+ return;
117
+ const parts = text.split("\x00");
118
+ if (parts.length !== 3)
119
+ return;
120
+ const [authorizationId = "", username = "", password = ""] = parts;
121
+ if (username === "" || password === "")
122
+ return;
123
+ return {
124
+ mechanism: "PLAIN",
125
+ username,
126
+ password,
127
+ ...authorizationId === "" || authorizationId === username ? {} : { authorizationId }
128
+ };
129
+ }
130
+ function decodeLoginStep2(response) {
131
+ return base64ToText(response);
132
+ }
133
+ var base64 = (text) => new TextEncoder().encode(text).toBase64();
134
+ function encodePlain(username, password) {
135
+ return base64(`\x00${username}\x00${password}`);
136
+ }
137
+ function encodeLoginStep(text) {
138
+ return base64(text);
139
+ }
140
+
141
+ // src/io/outgoing.ts
142
+ class Outgoing {
143
+ #write;
144
+ #queue = [];
145
+ #waiters = [];
146
+ constructor(write) {
147
+ this.#write = write;
148
+ }
149
+ get empty() {
150
+ return this.#queue.length === 0;
151
+ }
152
+ write(bytes) {
153
+ if (this.#queue.length > 0) {
154
+ this.#queue.push(bytes);
155
+ return;
156
+ }
157
+ const written = this.#write(bytes);
158
+ if (written < bytes.length) {
159
+ this.#queue.push(bytes.subarray(Math.max(written, 0)));
160
+ }
161
+ }
162
+ drain() {
163
+ while (this.#queue.length > 0) {
164
+ const bytes = this.#queue[0];
165
+ const written = this.#write(bytes);
166
+ if (written < bytes.length) {
167
+ this.#queue[0] = bytes.subarray(Math.max(written, 0));
168
+ return false;
169
+ }
170
+ this.#queue.shift();
171
+ }
172
+ for (const wake of this.#waiters.splice(0))
173
+ wake();
174
+ return true;
175
+ }
176
+ drained() {
177
+ if (this.#queue.length === 0)
178
+ return Promise.resolve();
179
+ return new Promise((wake) => this.#waiters.push(wake));
180
+ }
181
+ clear() {
182
+ this.#queue = [];
183
+ for (const wake of this.#waiters.splice(0))
184
+ wake();
185
+ }
186
+ }
187
+
188
+ // src/protocol/lines.ts
189
+ var MAX_LINE = 2048;
190
+
191
+ class LineSplitter {
192
+ #held = new Uint8Array(0);
193
+ #skipping = false;
194
+ #rest = new Uint8Array(0);
195
+ push(chunk) {
196
+ this.#rest = this.#held.length > 0 ? concat(this.#held, chunk) : chunk;
197
+ this.#held = new Uint8Array(0);
198
+ }
199
+ next() {
200
+ for (;; ) {
201
+ const data = this.#rest;
202
+ const lf = data.indexOf(10);
203
+ if (lf < 0) {
204
+ this.#rest = new Uint8Array(0);
205
+ if (this.#skipping)
206
+ return;
207
+ if (data.length > MAX_LINE) {
208
+ this.#skipping = true;
209
+ return { tooLong: true };
210
+ }
211
+ this.#held = data.slice();
212
+ return;
213
+ }
214
+ const end = lf > 0 && data[lf - 1] === 13 ? lf - 1 : lf;
215
+ this.#rest = data.subarray(lf + 1);
216
+ if (this.#skipping) {
217
+ this.#skipping = false;
218
+ continue;
219
+ }
220
+ if (end > MAX_LINE)
221
+ return { tooLong: true };
222
+ return { line: new TextDecoder().decode(data.subarray(0, end)) };
223
+ }
224
+ }
225
+ get holding() {
226
+ return this.#held.length > 0 || this.#rest.length > 0;
227
+ }
228
+ takeRest() {
229
+ const rest = this.#rest;
230
+ this.#rest = new Uint8Array(0);
231
+ return rest;
232
+ }
233
+ clear() {
234
+ this.#held = new Uint8Array(0);
235
+ this.#rest = new Uint8Array(0);
236
+ this.#skipping = false;
237
+ }
238
+ }
239
+ function concat(a, b) {
240
+ const out = new Uint8Array(a.length + b.length);
241
+ out.set(a, 0);
242
+ out.set(b, a.length);
243
+ return out;
244
+ }
245
+
246
+ export { SmtpError2, isHelloName, parsePath2, decodePlain2, decodeLoginStep2, encodePlain, encodeLoginStep, MAX_LINE, LineSplitter, Outgoing };
247
+
248
+ //# debugId=10D3BC4806ADB37C64756E2164756E21
249
+ //# sourceMappingURL=index-3423jxf9.js.map
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts", "../src/protocol/path.ts", "../src/protocol/sasl.ts", "../src/io/outgoing.ts", "../src/protocol/lines.ts"],
4
+ "sourcesContent": [
5
+ "import type { Reply } from './protocol/reply';\n\n/** Why the server refused an option, why a message stream ended in error, or why `sendMail` failed. */\nexport type SmtpErrorCode =\n\t| 'INVALID_OPTION'\n\t| 'ALREADY_LISTENING'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'BARE_LINE_BREAK'\n\t| 'CONNECTION_LOST'\n\t| 'HOOK_TIMEOUT'\n\t| 'INVALID_HOOK_REPLY'\n\t| 'MESSAGE_NOT_READ'\n\t| 'CONNECTION_FAILED'\n\t| 'TIMEOUT'\n\t| 'BAD_REPLY'\n\t| 'REFUSED'\n\t| 'RECIPIENTS_REFUSED'\n\t| 'TLS_UNAVAILABLE'\n\t| 'TLS_FAILED'\n\t| 'AUTH_UNAVAILABLE'\n\t| 'EXTENSION_MISSING'\n\t| 'NULL_MX'\n\t| 'DNS_FAILED';\n\n/** One recipient and the server's reply to its `RCPT TO`. */\nexport interface RecipientReply {\n\treadonly recipient: string;\n\treadonly reply: Reply;\n}\n\n/** What `sendMail` adds to an error: whether to try again, and the reply behind it. */\nexport interface SmtpErrorDetails {\n\t/** Trying again later may succeed: a 4xx, the network, a timeout. */\n\treadonly temporary?: boolean;\n\t/** The server's reply that failed the delivery, enhanced status code included. */\n\treadonly reply?: Reply;\n\t/** With `RECIPIENTS_REFUSED`: each recipient, and why it was refused. */\n\treadonly rejected?: readonly RecipientReply[];\n}\n\n/** The codes that are temporary unless a reply or `details` says otherwise. */\nconst TEMPORARY = new Set<SmtpErrorCode>([\n\t'CONNECTION_LOST',\n\t'CONNECTION_FAILED',\n\t'TIMEOUT',\n\t'BAD_REPLY',\n\t'TLS_UNAVAILABLE',\n\t'TLS_FAILED',\n]);\n\n/**\n * Thrown by `createSmtpServer` and `listen` for a wrong option or call; the\n * error a message's `content` stream ends with when the message must not be\n * delivered; what `onError` is given for a hook that broke a rule; and what\n * `sendMail` rejects with. `temporary` tells a failure worth trying again\n * later (a 4xx, the network, a timeout) from a permanent one (a 5xx, a null\n * MX, a message the server can never take).\n */\nexport class SmtpError extends Error {\n\toverride readonly name = 'SmtpError';\n\treadonly code: SmtpErrorCode;\n\t/** Trying again later may succeed. Meaningful for `sendMail`'s errors. */\n\treadonly temporary: boolean;\n\t/** The server reply behind a `sendMail` failure, enhanced status code included. */\n\treadonly reply?: Reply;\n\t/** With `RECIPIENTS_REFUSED`: each recipient, and the reply that refused it. */\n\treadonly rejected?: readonly RecipientReply[];\n\n\tconstructor(\n\t\tcode: SmtpErrorCode,\n\t\tmessage: string,\n\t\tdetails: SmtpErrorDetails = {},\n\t) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t\tthis.temporary =\n\t\t\tdetails.temporary ??\n\t\t\t(details.reply ? details.reply.code < 500 : TEMPORARY.has(code));\n\t\tif (details.reply) this.reply = details.reply;\n\t\tif (details.rejected) this.rejected = details.rejected;\n\t}\n}\n",
6
+ "/** An envelope address, as `MAIL FROM` and `RCPT TO` give it. */\nexport interface Path {\n\t/** `local@domain`; `''` for the null reverse-path `<>`. */\n\treadonly address: string;\n\treadonly local: string;\n\t/** Lower case. */\n\treadonly domain: string;\n}\n\nconst ATOM = /^[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\u0080-\\u{10ffff}]+$/u;\nconst LABEL =\n\t/^(?:[A-Za-z0-9\\u0080-\\u{10ffff}](?:[A-Za-z0-9\\-\\u0080-\\u{10ffff}]*[A-Za-z0-9\\u0080-\\u{10ffff}])?)$/u;\n\n/**\n * C1 controls, Unicode format characters (zero-width, bidi, BOM), the\n * line and paragraph separators, and lone surrogates: none has a place in\n * an address, and each would land as it is in the Received field and the\n * app's records — a lone surrogate as U+FFFD, once encoded for the wire.\n */\nconst INVISIBLE = /[\\u0080-\\u009f\\u2028\\u2029\\p{Cf}\\p{Cs}]/u;\n\n/**\n * C0 controls (CR, LF and NUL among them), DEL and `>`, anywhere in the\n * path, quoted or not: a CR or an LF would end the command line and start\n * another, and a `>` would end the path where a reader of the line stops.\n */\nfunction hasControl(text: string): boolean {\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tif (code < 0x20 || code === 0x7f || code === 0x3e) return true;\n\t}\n\treturn false;\n}\n\n/** One hop of a source route, `@domain`, and what follows it: `,` or `:`. */\nconst HOP = /^@(\\[[^\\]]*\\]|[^,:@[\\]]*)([,:])/;\n\nfunction isDotString(text: string): boolean {\n\treturn text.split('.').every((atom) => ATOM.test(atom));\n}\n\nfunction isQuotedString(text: string): boolean {\n\treturn /^\"(?:[\\x20\\x21\\x23-\\x5b\\x5d-\\x7e\\u0080-\\u{10ffff}]|\\\\[\\x20-\\x7e])*\"$/u.test(\n\t\ttext,\n\t);\n}\n\n/**\n * An address literal (§4.1.3): `[IPv6:…]`, its tag in any case (a quoted\n * string in RFC 5234 is), or `[a.b.c.d]` with each octet at most 255.\n */\nexport function isAddressLiteral(text: string): boolean {\n\tif (/^\\[IPv6:[0-9a-f:.]+\\]$/i.test(text)) return true;\n\tconst ipv4 = /^\\[(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\]$/.exec(text);\n\treturn ipv4?.slice(1).every((octet) => Number(octet) <= 255) === true;\n}\n\n/**\n * The argument of EHLO or HELO (RFC 5321 §4.1.1.1): a host name of\n * letters, digits and hyphens, or an address literal.\n */\nexport function isHelloName(text: string): boolean {\n\treturn (\n\t\t/^[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$/.test(text) || isAddressLiteral(text)\n\t);\n}\n\nfunction isDomain(domain: string): boolean {\n\tif (isAddressLiteral(domain)) return true;\n\treturn (\n\t\tdomain.length <= 255 &&\n\t\tdomain.split('.').every((label) => label.length <= 63 && LABEL.test(label))\n\t);\n}\n\n/**\n * What `parsePath` does with a source route (`<@a,@b:local@domain>`, RFC 5321\n * Appendix C): a server accepts and discards it (`'discard'`), a client\n * refuses it, as §4.1.1.3 says clients should not send one (`'refuse'`).\n */\nexport type SourceRoute = 'discard' | 'refuse';\n\n/**\n * The mailbox after a source route, each of whose hops must be\n * `@domain` with a valid domain; `undefined` for any other route.\n */\nfunction afterRoute(inner: string): string | undefined {\n\tlet rest = inner;\n\tfor (;;) {\n\t\tconst hop = HOP.exec(rest);\n\t\tif (!hop || !isDomain(hop[1] as string)) return undefined;\n\t\trest = rest.slice(hop[0].length);\n\t\tif (hop[2] === ':') return rest;\n\t}\n}\n\n/**\n * Parses the `<path>` of RFC 5321 §4.1.2: `<local@domain>`, or `<>` when\n * `allowNull`. A source route (`<@a,@b:local@domain>`, §C) is dropped when\n * `sourceRoute` is `'discard'`, the default, provided each hop is\n * `@domain`; with `'refuse'` the path is not one. A C0 control (CR, LF,\n * NUL…), DEL or `>` anywhere refuses the path, as do C1 controls,\n * Unicode format characters and lone surrogates. An IPv4 address literal\n * takes octets up to 255.\n * Non-ASCII is allowed: whether the session may use it is SMTPUTF8's\n * question (RFC 6531), not the grammar's. `undefined` when it is not a path.\n *\n * The local part is kept as written, quotes included: `\"v@x.example\"` and\n * `v%x.example` are local parts of the domain after the last `@`, not\n * addresses elsewhere. Code that delivers must not split on the first `@`.\n */\nexport function parsePath(\n\ttext: string,\n\tallowNull: boolean,\n\tsourceRoute: SourceRoute = 'discard',\n): Path | undefined {\n\tif (!text.startsWith('<') || !text.endsWith('>')) return undefined;\n\tlet inner: string | undefined = text.slice(1, -1);\n\tif (hasControl(inner) || INVISIBLE.test(inner)) return undefined;\n\tif (inner === '')\n\t\treturn allowNull ? { address: '', local: '', domain: '' } : undefined;\n\tif (inner.startsWith('@')) {\n\t\tif (sourceRoute === 'refuse') return undefined;\n\t\tinner = afterRoute(inner);\n\t\tif (inner === undefined) return undefined;\n\t}\n\tconst at = inner.lastIndexOf('@');\n\tif (at <= 0) return undefined;\n\tconst local = inner.slice(0, at);\n\tconst domain = inner.slice(at + 1);\n\tif (local.length > 64 || !(isDotString(local) || isQuotedString(local)))\n\t\treturn undefined;\n\tif (!isDomain(domain)) return undefined;\n\treturn {\n\t\taddress: `${local}@${domain.toLowerCase()}`,\n\t\tlocal,\n\t\tdomain: domain.toLowerCase(),\n\t};\n}\n",
7
+ "/** Credentials a client gave with `AUTH`. */\nexport interface Credentials {\n\treadonly mechanism: 'PLAIN' | 'LOGIN';\n\treadonly username: string;\n\treadonly password: string;\n\t/**\n\t * PLAIN's authorization identity, when it differs from the username.\n\t * The server refuses such credentials before `authenticate` is asked.\n\t */\n\treadonly authorizationId?: string;\n}\n\nfunction base64ToText(text: string): string | undefined {\n\tif (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0)\n\t\treturn undefined;\n\ttry {\n\t\treturn new TextDecoder('utf-8', { fatal: true }).decode(\n\t\t\tUint8Array.fromBase64(text),\n\t\t);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** PLAIN (RFC 4616): `authzid NUL authcid NUL passwd`, in base64. */\nexport function decodePlain(response: string): Credentials | undefined {\n\tconst text = base64ToText(response);\n\tif (text === undefined) return undefined;\n\tconst parts = text.split('\\0');\n\tif (parts.length !== 3) return undefined;\n\tconst [authorizationId = '', username = '', password = ''] = parts;\n\tif (username === '' || password === '') return undefined;\n\treturn {\n\t\tmechanism: 'PLAIN',\n\t\tusername,\n\t\tpassword,\n\t\t...(authorizationId === '' || authorizationId === username\n\t\t\t? {}\n\t\t\t: { authorizationId }),\n\t};\n}\n\n/** One step of LOGIN: a base64 line to its text. */\nexport function decodeLoginStep(response: string): string | undefined {\n\treturn base64ToText(response);\n}\n\nconst base64 = (text: string) => new TextEncoder().encode(text).toBase64();\n\n/** A client's PLAIN response (RFC 4616): `NUL username NUL password`, in base64, no authorization identity. */\nexport function encodePlain(username: string, password: string): string {\n\treturn base64(`\\0${username}\\0${password}`);\n}\n\n/** One step of LOGIN, the username or the password, in base64. */\nexport function encodeLoginStep(text: string): string {\n\treturn base64(text);\n}\n",
8
+ "/**\n * What waits to leave a Bun socket. Bun's sockets do not buffer: `write`\n * takes what the kernel takes and says how much. The rest waits here for\n * the socket's `drain` event, so nothing written is lost to a peer that\n * reads slowly. The server's transport and the client's socket share it.\n */\nexport class Outgoing {\n\treadonly #write: (bytes: Uint8Array) => number;\n\t#queue: Uint8Array[] = [];\n\t#waiters: (() => void)[] = [];\n\n\tconstructor(write: (bytes: Uint8Array) => number) {\n\t\tthis.#write = write;\n\t}\n\n\t/** Nothing waits to leave. */\n\tget empty(): boolean {\n\t\treturn this.#queue.length === 0;\n\t}\n\n\t/** Writes what the socket takes now, and keeps the rest for `drain`. */\n\twrite(bytes: Uint8Array): void {\n\t\tif (this.#queue.length > 0) {\n\t\t\tthis.#queue.push(bytes);\n\t\t\treturn;\n\t\t}\n\t\tconst written = this.#write(bytes);\n\t\tif (written < bytes.length) {\n\t\t\tthis.#queue.push(bytes.subarray(Math.max(written, 0)));\n\t\t}\n\t}\n\n\t/** The socket's `drain`: it can take more. True once everything has left. */\n\tdrain(): boolean {\n\t\twhile (this.#queue.length > 0) {\n\t\t\tconst bytes = this.#queue[0] as Uint8Array;\n\t\t\tconst written = this.#write(bytes);\n\t\t\tif (written < bytes.length) {\n\t\t\t\tthis.#queue[0] = bytes.subarray(Math.max(written, 0));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthis.#queue.shift();\n\t\t}\n\t\tfor (const wake of this.#waiters.splice(0)) wake();\n\t\treturn true;\n\t}\n\n\t/** Resolves once everything written has left, or the socket closed. */\n\tdrained(): Promise<void> {\n\t\tif (this.#queue.length === 0) return Promise.resolve();\n\t\treturn new Promise((wake) => this.#waiters.push(wake));\n\t}\n\n\t/** The socket closed: nothing more will leave. */\n\tclear(): void {\n\t\tthis.#queue = [];\n\t\tfor (const wake of this.#waiters.splice(0)) wake();\n\t}\n}\n",
9
+ "/**\n * The longest line taken, CRLF included: a command (RFC 5321 §4.5.3.1.4\n * asks for 512 at least) or a reply line (§4.5.3.1.5, 512 too).\n */\nexport const MAX_LINE = 2048;\n\n/** One line from the peer, or a line that was too long and was skipped. */\nexport type LineEvent = { line: string } | { tooLong: true };\n\n/**\n * Cuts a peer's bytes into lines — a client's commands, a server's\n * replies — ending at LF with or without CR. It holds one partial line at most: past `MAX_LINE`, the line is\n * skipped to its end and reported once.\n */\nexport class LineSplitter {\n\t#held: Uint8Array = new Uint8Array(0);\n\t#skipping = false;\n\t#rest: Uint8Array = new Uint8Array(0);\n\n\t/** Starts on a chunk; read its lines with `next`. */\n\tpush(chunk: Uint8Array): void {\n\t\tthis.#rest = this.#held.length > 0 ? concat(this.#held, chunk) : chunk;\n\t\tthis.#held = new Uint8Array(0);\n\t}\n\n\t/** The next line of the current chunk, or `undefined` when it has no more. */\n\tnext(): LineEvent | undefined {\n\t\tfor (;;) {\n\t\t\tconst data = this.#rest;\n\t\t\tconst lf = data.indexOf(0x0a);\n\t\t\tif (lf < 0) {\n\t\t\t\tthis.#rest = new Uint8Array(0);\n\t\t\t\tif (this.#skipping) return undefined;\n\t\t\t\tif (data.length > MAX_LINE) {\n\t\t\t\t\tthis.#skipping = true;\n\t\t\t\t\treturn { tooLong: true };\n\t\t\t\t}\n\t\t\t\tthis.#held = data.slice();\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst end = lf > 0 && data[lf - 1] === 0x0d ? lf - 1 : lf;\n\t\t\tthis.#rest = data.subarray(lf + 1);\n\t\t\tif (this.#skipping) {\n\t\t\t\tthis.#skipping = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (end > MAX_LINE) return { tooLong: true };\n\t\t\treturn { line: new TextDecoder().decode(data.subarray(0, end)) };\n\t\t}\n\t}\n\n\t/** Bytes are held or left: a line begun, or lines not yet read. */\n\tget holding(): boolean {\n\t\treturn this.#held.length > 0 || this.#rest.length > 0;\n\t}\n\n\t/** What is left of the current chunk: message content pipelined after DATA. */\n\ttakeRest(): Uint8Array {\n\t\tconst rest = this.#rest;\n\t\tthis.#rest = new Uint8Array(0);\n\t\treturn rest;\n\t}\n\n\t/** Forgets everything held: after STARTTLS, or after a refused DATA. */\n\tclear(): void {\n\t\tthis.#held = new Uint8Array(0);\n\t\tthis.#rest = new Uint8Array(0);\n\t\tthis.#skipping = false;\n\t}\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n\tconst out = new Uint8Array(a.length + b.length);\n\tout.set(a, 0);\n\tout.set(b, a.length);\n\treturn out;\n}\n"
10
+ ],
11
+ "mappings": ";AAyCA,IAAM,YAAY,IAAI,IAAmB;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAAA;AAUM,MAAM,mBAAkB,MAAM;AAAA,EAClB,OAAO;AAAA,EAChB;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,EAET,WAAW,CACV,MACA,SACA,UAA4B,CAAC,GAC5B;AAAA,IACD,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,KAAK,YACJ,QAAQ,cACP,QAAQ,QAAQ,QAAQ,MAAM,OAAO,MAAM,UAAU,IAAI,IAAI;AAAA,IAC/D,IAAI,QAAQ;AAAA,MAAO,KAAK,QAAQ,QAAQ;AAAA,IACxC,IAAI,QAAQ;AAAA,MAAU,KAAK,WAAW,QAAQ;AAAA;AAEhD;;;ACxEA,IAAM,OAAO;AACb,IAAM,QACL;AAQD,IAAM,YAAY;AAOlB,SAAS,UAAU,CAAC,MAAuB;AAAA,EAC1C,SAAS,IAAI,EAAG,IAAI,KAAK,QAAQ,KAAK;AAAA,IACrC,MAAM,OAAO,KAAK,WAAW,CAAC;AAAA,IAC9B,IAAI,OAAO,MAAQ,SAAS,OAAQ,SAAS;AAAA,MAAM,OAAO;AAAA,EAC3D;AAAA,EACA,OAAO;AAAA;AAIR,IAAM,MAAM;AAEZ,SAAS,WAAW,CAAC,MAAuB;AAAA,EAC3C,OAAO,KAAK,MAAM,GAAG,EAAE,MAAM,CAAC,SAAS,KAAK,KAAK,IAAI,CAAC;AAAA;AAGvD,SAAS,cAAc,CAAC,MAAuB;AAAA,EAC9C,OAAO,wEAAwE,KAC9E,IACD;AAAA;AAOM,SAAS,gBAAgB,CAAC,MAAuB;AAAA,EACvD,IAAI,0BAA0B,KAAK,IAAI;AAAA,IAAG,OAAO;AAAA,EACjD,MAAM,OAAO,mDAAmD,KAAK,IAAI;AAAA,EACzE,OAAO,MAAM,MAAM,CAAC,EAAE,MAAM,CAAC,UAAU,OAAO,KAAK,KAAK,GAAG,MAAM;AAAA;AAO3D,SAAS,WAAW,CAAC,MAAuB;AAAA,EAClD,OACC,sCAAsC,KAAK,IAAI,KAAK,iBAAiB,IAAI;AAAA;AAI3E,SAAS,QAAQ,CAAC,QAAyB;AAAA,EAC1C,IAAI,iBAAiB,MAAM;AAAA,IAAG,OAAO;AAAA,EACrC,OACC,OAAO,UAAU,OACjB,OAAO,MAAM,GAAG,EAAE,MAAM,CAAC,UAAU,MAAM,UAAU,MAAM,MAAM,KAAK,KAAK,CAAC;AAAA;AAe5E,SAAS,UAAU,CAAC,OAAmC;AAAA,EACtD,IAAI,OAAO;AAAA,EACX,UAAS;AAAA,IACR,MAAM,MAAM,IAAI,KAAK,IAAI;AAAA,IACzB,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,EAAY;AAAA,MAAG;AAAA,IACzC,OAAO,KAAK,MAAM,IAAI,GAAG,MAAM;AAAA,IAC/B,IAAI,IAAI,OAAO;AAAA,MAAK,OAAO;AAAA,EAC5B;AAAA;AAkBM,SAAS,UAAS,CACxB,MACA,WACA,cAA2B,WACR;AAAA,EACnB,IAAI,CAAC,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG;AAAA,IAAG;AAAA,EAClD,IAAI,QAA4B,KAAK,MAAM,GAAG,EAAE;AAAA,EAChD,IAAI,WAAW,KAAK,KAAK,UAAU,KAAK,KAAK;AAAA,IAAG;AAAA,EAChD,IAAI,UAAU;AAAA,IACb,OAAO,YAAY,EAAE,SAAS,IAAI,OAAO,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC7D,IAAI,MAAM,WAAW,GAAG,GAAG;AAAA,IAC1B,IAAI,gBAAgB;AAAA,MAAU;AAAA,IAC9B,QAAQ,WAAW,KAAK;AAAA,IACxB,IAAI,UAAU;AAAA,MAAW;AAAA,EAC1B;AAAA,EACA,MAAM,KAAK,MAAM,YAAY,GAAG;AAAA,EAChC,IAAI,MAAM;AAAA,IAAG;AAAA,EACb,MAAM,QAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,EAC/B,MAAM,SAAS,MAAM,MAAM,KAAK,CAAC;AAAA,EACjC,IAAI,MAAM,SAAS,MAAM,EAAE,YAAY,KAAK,KAAK,eAAe,KAAK;AAAA,IACpE;AAAA,EACD,IAAI,CAAC,SAAS,MAAM;AAAA,IAAG;AAAA,EACvB,OAAO;AAAA,IACN,SAAS,GAAG,SAAS,OAAO,YAAY;AAAA,IACxC;AAAA,IACA,QAAQ,OAAO,YAAY;AAAA,EAC5B;AAAA;;;AC7HD,SAAS,YAAY,CAAC,MAAkC;AAAA,EACvD,IAAI,CAAC,yBAAyB,KAAK,IAAI,KAAK,KAAK,SAAS,MAAM;AAAA,IAC/D;AAAA,EACD,IAAI;AAAA,IACH,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAChD,WAAW,WAAW,IAAI,CAC3B;AAAA,IACC,MAAM;AAAA,IACP;AAAA;AAAA;AAKK,SAAS,YAAW,CAAC,UAA2C;AAAA,EACtE,MAAM,OAAO,aAAa,QAAQ;AAAA,EAClC,IAAI,SAAS;AAAA,IAAW;AAAA,EACxB,MAAM,QAAQ,KAAK,MAAM,MAAI;AAAA,EAC7B,IAAI,MAAM,WAAW;AAAA,IAAG;AAAA,EACxB,OAAO,kBAAkB,IAAI,WAAW,IAAI,WAAW,MAAM;AAAA,EAC7D,IAAI,aAAa,MAAM,aAAa;AAAA,IAAI;AAAA,EACxC,OAAO;AAAA,IACN,WAAW;AAAA,IACX;AAAA,IACA;AAAA,OACI,oBAAoB,MAAM,oBAAoB,WAC/C,CAAC,IACD,EAAE,gBAAgB;AAAA,EACtB;AAAA;AAIM,SAAS,gBAAe,CAAC,UAAsC;AAAA,EACrE,OAAO,aAAa,QAAQ;AAAA;AAG7B,IAAM,SAAS,CAAC,SAAiB,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE,SAAS;AAGlE,SAAS,WAAW,CAAC,UAAkB,UAA0B;AAAA,EACvE,OAAO,OAAO,OAAK,eAAa,UAAU;AAAA;AAIpC,SAAS,eAAe,CAAC,MAAsB;AAAA,EACrD,OAAO,OAAO,IAAI;AAAA;;;AClDZ,MAAM,SAAS;AAAA,EACZ;AAAA,EACT,SAAuB,CAAC;AAAA,EACxB,WAA2B,CAAC;AAAA,EAE5B,WAAW,CAAC,OAAsC;AAAA,IACjD,KAAK,SAAS;AAAA;AAAA,MAIX,KAAK,GAAY;AAAA,IACpB,OAAO,KAAK,OAAO,WAAW;AAAA;AAAA,EAI/B,KAAK,CAAC,OAAyB;AAAA,IAC9B,IAAI,KAAK,OAAO,SAAS,GAAG;AAAA,MAC3B,KAAK,OAAO,KAAK,KAAK;AAAA,MACtB;AAAA,IACD;AAAA,IACA,MAAM,UAAU,KAAK,OAAO,KAAK;AAAA,IACjC,IAAI,UAAU,MAAM,QAAQ;AAAA,MAC3B,KAAK,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC;AAAA,IACtD;AAAA;AAAA,EAID,KAAK,GAAY;AAAA,IAChB,OAAO,KAAK,OAAO,SAAS,GAAG;AAAA,MAC9B,MAAM,QAAQ,KAAK,OAAO;AAAA,MAC1B,MAAM,UAAU,KAAK,OAAO,KAAK;AAAA,MACjC,IAAI,UAAU,MAAM,QAAQ;AAAA,QAC3B,KAAK,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI,SAAS,CAAC,CAAC;AAAA,QACpD,OAAO;AAAA,MACR;AAAA,MACA,KAAK,OAAO,MAAM;AAAA,IACnB;AAAA,IACA,WAAW,QAAQ,KAAK,SAAS,OAAO,CAAC;AAAA,MAAG,KAAK;AAAA,IACjD,OAAO;AAAA;AAAA,EAIR,OAAO,GAAkB;AAAA,IACxB,IAAI,KAAK,OAAO,WAAW;AAAA,MAAG,OAAO,QAAQ,QAAQ;AAAA,IACrD,OAAO,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA;AAAA,EAItD,KAAK,GAAS;AAAA,IACb,KAAK,SAAS,CAAC;AAAA,IACf,WAAW,QAAQ,KAAK,SAAS,OAAO,CAAC;AAAA,MAAG,KAAK;AAAA;AAEnD;;;ACtDO,IAAM,WAAW;AAAA;AAUjB,MAAM,aAAa;AAAA,EACzB,QAAoB,IAAI,WAAW,CAAC;AAAA,EACpC,YAAY;AAAA,EACZ,QAAoB,IAAI,WAAW,CAAC;AAAA,EAGpC,IAAI,CAAC,OAAyB;AAAA,IAC7B,KAAK,QAAQ,KAAK,MAAM,SAAS,IAAI,OAAO,KAAK,OAAO,KAAK,IAAI;AAAA,IACjE,KAAK,QAAQ,IAAI,WAAW,CAAC;AAAA;AAAA,EAI9B,IAAI,GAA0B;AAAA,IAC7B,UAAS;AAAA,MACR,MAAM,OAAO,KAAK;AAAA,MAClB,MAAM,KAAK,KAAK,QAAQ,EAAI;AAAA,MAC5B,IAAI,KAAK,GAAG;AAAA,QACX,KAAK,QAAQ,IAAI,WAAW,CAAC;AAAA,QAC7B,IAAI,KAAK;AAAA,UAAW;AAAA,QACpB,IAAI,KAAK,SAAS,UAAU;AAAA,UAC3B,KAAK,YAAY;AAAA,UACjB,OAAO,EAAE,SAAS,KAAK;AAAA,QACxB;AAAA,QACA,KAAK,QAAQ,KAAK,MAAM;AAAA,QACxB;AAAA,MACD;AAAA,MACA,MAAM,MAAM,KAAK,KAAK,KAAK,KAAK,OAAO,KAAO,KAAK,IAAI;AAAA,MACvD,KAAK,QAAQ,KAAK,SAAS,KAAK,CAAC;AAAA,MACjC,IAAI,KAAK,WAAW;AAAA,QACnB,KAAK,YAAY;AAAA,QACjB;AAAA,MACD;AAAA,MACA,IAAI,MAAM;AAAA,QAAU,OAAO,EAAE,SAAS,KAAK;AAAA,MAC3C,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,SAAS,GAAG,GAAG,CAAC,EAAE;AAAA,IAChE;AAAA;AAAA,MAIG,OAAO,GAAY;AAAA,IACtB,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS;AAAA;AAAA,EAIrD,QAAQ,GAAe;AAAA,IACtB,MAAM,OAAO,KAAK;AAAA,IAClB,KAAK,QAAQ,IAAI,WAAW,CAAC;AAAA,IAC7B,OAAO;AAAA;AAAA,EAIR,KAAK,GAAS;AAAA,IACb,KAAK,QAAQ,IAAI,WAAW,CAAC;AAAA,IAC7B,KAAK,QAAQ,IAAI,WAAW,CAAC;AAAA,IAC7B,KAAK,YAAY;AAAA;AAEnB;AAEA,SAAS,MAAM,CAAC,GAAe,GAA2B;AAAA,EACzD,MAAM,MAAM,IAAI,WAAW,EAAE,SAAS,EAAE,MAAM;AAAA,EAC9C,IAAI,IAAI,GAAG,CAAC;AAAA,EACZ,IAAI,IAAI,GAAG,EAAE,MAAM;AAAA,EACnB,OAAO;AAAA;",
12
+ "debugId": "10D3BC4806ADB37C64756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,25 @@
1
+ import { SmtpError } from '../errors';
2
+ import type { MessageSource } from './options';
3
+ /** The message's bytes, one slice at a time; `undefined` once they end. */
4
+ export interface Chunks {
5
+ next(): Promise<Uint8Array | undefined>;
6
+ /** Stops reading: a delivery that failed half-way. */
7
+ cancel(): void;
8
+ }
9
+ /** A message ready to send: what is known of it before the first byte, and its bytes. */
10
+ export interface Content {
11
+ /** Bytes, when known before sending: SIZE declares it. */
12
+ readonly size?: number;
13
+ /** A byte above 127, when known before sending: it needs 8BITMIME. */
14
+ readonly eightBit: boolean;
15
+ chunks(): Chunks;
16
+ }
17
+ /** The error for a bare CR or LF in the message: a server must refuse it, and this client does first. */
18
+ export declare const bareLineBreak: () => SmtpError;
19
+ /**
20
+ * The message, checked before any connection when it is in memory: a bare
21
+ * CR or LF is refused here (unless normalised), and its size and 8-bit
22
+ * bytes are known for MAIL FROM. A stream is checked as it is sent.
23
+ */
24
+ export declare function contentOf(message: MessageSource, normalize: boolean, size: number | undefined): Content;
25
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/client/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAK/C,2EAA2E;AAC3E,MAAM,WAAW,MAAM;IACtB,IAAI,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IACxC,sDAAsD;IACtD,MAAM,IAAI,IAAI,CAAC;CACf;AAED,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,MAAM,IAAI,MAAM,CAAC;CACjB;AAED,yGAAyG;AACzG,eAAO,MAAM,aAAa,iBAIxB,CAAC;AA2CH;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,OAAO,EAClB,IAAI,EAAE,MAAM,GAAG,SAAS,GACtB,OAAO,CAwCT"}
@@ -0,0 +1,18 @@
1
+ import type { Reply } from '../protocol/reply';
2
+ /** The most bytes a server may send in one session, every reply together. */
3
+ export declare const MAX_REPLY_BYTES: number;
4
+ /**
5
+ * What a server sent, put together into replies, each bounded: `MAX_LINE`
6
+ * a line, `MAX_REPLY_LINES` a reply, `MAX_REPLY_BYTES` in all. One pass
7
+ * over the bytes, whatever the chunks.
8
+ */
9
+ export declare class Inbox {
10
+ #private;
11
+ /** Takes the next bytes; says what is wrong with them, if they are not replies. */
12
+ push(chunk: Uint8Array): string | undefined;
13
+ /** The next complete reply. */
14
+ shift(): Reply | undefined;
15
+ /** Bytes or replies not yet read. */
16
+ get holding(): boolean;
17
+ }
18
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/client/inbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,6EAA6E;AAC7E,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,KAAK;;IAMjB,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS;IAiB3C,+BAA+B;IAC/B,KAAK,IAAI,KAAK,GAAG,SAAS;IAI1B,qCAAqC;IACrC,IAAI,OAAO,IAAI,OAAO,CAErB;CACD"}
@@ -0,0 +1,6 @@
1
+ export { type RecipientReply, SmtpError, type SmtpErrorCode, type SmtpErrorDetails, } from '../errors';
2
+ export type { Reply } from '../protocol/reply';
3
+ export { type MailHost, resolveMx } from './mx';
4
+ export type { HostDestination, MessageSource, MxDestination, MxResolver, SendMailAuth, SendMailEnvelope, SendMailOptions, SendMailResult, SendMailTimeouts, TlsMode, } from './options';
5
+ export { sendMail } from './send';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAChD,YAAY,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,OAAO,GACP,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC"}