@bumail/smtp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -6
- package/dist/chunks/index-2ybj2z1w.js +216 -0
- package/dist/chunks/index-2ybj2z1w.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 +945 -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.js +115 -196
- 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/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/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 +21 -4
- package/dist/server/transport.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide.md +381 -9
- package/docs/roadmap.md +43 -8
- package/docs/troubleshooting.md +668 -7
- package/package.json +12 -4
- package/dist/server/lines.d.ts.map +0 -1
package/docs/troubleshooting.md
CHANGED
|
@@ -4,6 +4,10 @@ Each entry is headed by the text you see: the message of the error thrown,
|
|
|
4
4
|
or the reply the server sends, as a client or a log prints it. `…` stands
|
|
5
5
|
for the part that varies, often the server's `hostname`.
|
|
6
6
|
|
|
7
|
+
What `sendMail` (`@bumail/smtp/client`) rejects with is under *Sending
|
|
8
|
+
mail*. There `…` is often the host the client spoke to — `host`, or the MX
|
|
9
|
+
host — and an error's `temporary` says whether to retry it later.
|
|
10
|
+
|
|
7
11
|
A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
|
|
8
12
|
`EHLO`. After `HELO`, or before either, the same reply comes without it:
|
|
9
13
|
`554 Relay access denied`, not `554 5.7.1 Relay access denied`.
|
|
@@ -17,6 +21,7 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
|
|
|
17
21
|
- [`SmtpError: createSmtpServer(): authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted`](#smtperror-createsmtpserver-authenticate-needs-tls--key-cert--since-auth-is-offered-only-once-encrypted)
|
|
18
22
|
- [`SmtpError: createSmtpServer(): localDomains must be an array of domains or a function`](#smtperror-createsmtpserver-localdomains-must-be-an-array-of-domains-or-a-function)
|
|
19
23
|
- [`SmtpError: createSmtpServer(): … must be a positive integer, not …`](#smtperror-createsmtpserver--must-be-a-positive-integer-not-)
|
|
24
|
+
- [`SmtpError: createSmtpServer(): hookTimeout must be at most 2147483 seconds, not …`](#smtperror-createsmtpserver-hooktimeout-must-be-at-most-2147483-seconds-not-)
|
|
20
25
|
- [`SmtpError: createSmtpServer(): greetingDelay must be a number of seconds, 0 or more, not …`](#smtperror-createsmtpserver-greetingdelay-must-be-a-number-of-seconds-0-or-more-not-)
|
|
21
26
|
- [`SmtpError: createSmtpServer(): greetingDelay (… s) must be shorter than timeout (… s), or every client times out before the greeting`](#smtperror-createsmtpserver-greetingdelay--s-must-be-shorter-than-timeout--s-or-every-client-times-out-before-the-greeting)
|
|
22
27
|
- [`SmtpError: listen(): the server is already listening on …`](#smtperror-listen-the-server-is-already-listening-on-)
|
|
@@ -81,6 +86,67 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
|
|
|
81
86
|
- [`421 4.4.2 … Idle too long, closing`](#421-442--idle-too-long-closing)
|
|
82
87
|
- [`554 … Talked before the greeting`](#554--talked-before-the-greeting)
|
|
83
88
|
|
|
89
|
+
**Sending mail: options**
|
|
90
|
+
|
|
91
|
+
- [`SmtpError: sendMail(): "…" is not an address (local@domain)`](#smtperror-sendmail--is-not-an-address-localdomain)
|
|
92
|
+
- [`SmtpError: sendMail(): to must name one recipient or more`](#smtperror-sendmail-to-must-name-one-recipient-or-more)
|
|
93
|
+
- [`SmtpError: sendMail(): helo "…" is not a host name`](#smtperror-sendmail-helo--is-not-a-host-name)
|
|
94
|
+
- [`SmtpError: sendMail(): helo is required for delivery by MX: pass your server's public name, such as helo: 'mail.example.com'`](#smtperror-sendmail-helo-is-required-for-delivery-by-mx-pass-your-servers-public-name-such-as-helo-mailexamplecom)
|
|
95
|
+
- [`SmtpError: sendMail(): … must be a number of seconds above 0 and at most 2147483, not …`](#smtperror-sendmail--must-be-a-number-of-seconds-above-0-and-at-most-2147483-not-)
|
|
96
|
+
- [`SmtpError: sendMail(): tls must be 'opportunistic', 'required' or 'none', not …`](#smtperror-sendmail-tls-must-be-opportunistic-required-or-none-not-)
|
|
97
|
+
- [`SmtpError: sendMail(): secure is TLS from the first byte: it cannot go with tls: 'none'`](#smtperror-sendmail-secure-is-tls-from-the-first-byte-it-cannot-go-with-tls-none)
|
|
98
|
+
- [`SmtpError: sendMail(): auth with tls: 'none' would send the password in clear; pass allowPlaintextAuth: true for a local test server`](#smtperror-sendmail-auth-with-tls-none-would-send-the-password-in-clear-pass-allowplaintextauth-true-for-a-local-test-server)
|
|
99
|
+
- [`SmtpError: sendMail(): auth with tls: 'opportunistic' would send the password to a server whose certificate is not checked; leave tls out to check it, or pass allowPlaintextAuth: true for a local test server`](#smtperror-sendmail-auth-with-tls-opportunistic-would-send-the-password-to-a-server-whose-certificate-is-not-checked-leave-tls-out-to-check-it-or-pass-allowplaintextauth-true-for-a-local-test-server)
|
|
100
|
+
- [`SmtpError: sendMail(): auth.username must be a non-empty string`](#smtperror-sendmail-authusername-must-be-a-non-empty-string)
|
|
101
|
+
- [`SmtpError: sendMail(): auth.password must be a non-empty string`](#smtperror-sendmail-authpassword-must-be-a-non-empty-string)
|
|
102
|
+
- [`SmtpError: sendMail(): auth.mechanism must be 'PLAIN' or 'LOGIN', not …`](#smtperror-sendmail-authmechanism-must-be-plain-or-login-not-)
|
|
103
|
+
- [`SmtpError: sendMail(): size must be a number of bytes, not …`](#smtperror-sendmail-size-must-be-a-number-of-bytes-not-)
|
|
104
|
+
- [`SmtpError: sendMail(): port must be an integer from 1 to 65535, not …`](#smtperror-sendmail-port-must-be-an-integer-from-1-to-65535-not-)
|
|
105
|
+
- [`SmtpError: sendMail(): host must be a host name or an address, not …`](#smtperror-sendmail-host-must-be-a-host-name-or-an-address-not-)
|
|
106
|
+
- [`SmtpError: sendMail(): domain must be a domain name`](#smtperror-sendmail-domain-must-be-a-domain-name)
|
|
107
|
+
- [`SmtpError: sendMail(): resolver must be a Resolver, such as @bumail/dns gives`](#smtperror-sendmail-resolver-must-be-a-resolver-such-as-bumaildns-gives)
|
|
108
|
+
- [`SmtpError: sendMail(): options must be an object`](#smtperror-sendmail-options-must-be-an-object)
|
|
109
|
+
- [`SmtpError: sendMail(): the message must be a Uint8Array, a string or a ReadableStream`](#smtperror-sendmail-the-message-must-be-a-uint8array-a-string-or-a-readablestream)
|
|
110
|
+
- [`SmtpError: sendMail(): a message stream must give Uint8Array chunks`](#smtperror-sendmail-a-message-stream-must-give-uint8array-chunks)
|
|
111
|
+
- [`SmtpError: sendMail(): a message stream can be sent once only`](#smtperror-sendmail-a-message-stream-can-be-sent-once-only)
|
|
112
|
+
|
|
113
|
+
**Sending mail: the message**
|
|
114
|
+
|
|
115
|
+
- [`SmtpError: sendMail(): the message holds a bare CR or LF, which a server must refuse (SMTP smuggling); end every line with CRLF, or pass normalizeLineEnds: true`](#smtperror-sendmail-the-message-holds-a-bare-cr-or-lf-which-a-server-must-refuse-smtp-smuggling-end-every-line-with-crlf-or-pass-normalizelineends-true)
|
|
116
|
+
- [`SmtpError: The message (… bytes) is larger than the … bytes … takes (SIZE)`](#smtperror-the-message--bytes-is-larger-than-the--bytes--takes-size)
|
|
117
|
+
- [`SmtpError: … does not offer 8BITMIME, which the message needs: it has 8-bit bytes`](#smtperror--does-not-offer-8bitmime-which-the-message-needs-it-has-8-bit-bytes)
|
|
118
|
+
- [`SmtpError: … does not offer SMTPUTF8, which an address that is not ASCII needs (or smtputf8 asked for)`](#smtperror--does-not-offer-smtputf8-which-an-address-that-is-not-ascii-needs-or-smtputf8-asked-for)
|
|
119
|
+
|
|
120
|
+
**Sending mail: connections, TLS and AUTH**
|
|
121
|
+
|
|
122
|
+
- [`SmtpError: Could not connect to … (…:…): …`](#smtperror-could-not-connect-to---)
|
|
123
|
+
- [`SmtpError: Timed out after … s waiting for … (…)`](#smtperror-timed-out-after--s-waiting-for--)
|
|
124
|
+
- [`SmtpError: The deadline of … s passed waiting for … (…)`](#smtperror-the-deadline-of--s-passed-waiting-for--)
|
|
125
|
+
- [`SmtpError: The connection to … closed before …`](#smtperror-the-connection-to--closed-before-)
|
|
126
|
+
- [`SmtpError: The connection to … closed while the message was sent`](#smtperror-the-connection-to--closed-while-the-message-was-sent)
|
|
127
|
+
- [`SmtpError: TLS with … failed: …`](#smtperror-tls-with--failed-)
|
|
128
|
+
- [`SmtpError: … does not offer STARTTLS, and tls is 'required'`](#smtperror--does-not-offer-starttls-and-tls-is-required)
|
|
129
|
+
- [`SmtpError: … refused STARTTLS, and tls is 'required': …`](#smtperror--refused-starttls-and-tls-is-required-)
|
|
130
|
+
- [`SmtpError: Refusing to send credentials to … in clear: it did not start TLS (allowPlaintextAuth is for a local test server only)`](#smtperror-refusing-to-send-credentials-to--in-clear-it-did-not-start-tls-allowplaintextauth-is-for-a-local-test-server-only)
|
|
131
|
+
- [`SmtpError: … offers no AUTH mechanism this client speaks (…): AUTH …`](#smtperror--offers-no-auth-mechanism-this-client-speaks--auth-)
|
|
132
|
+
|
|
133
|
+
**Sending mail: replies**
|
|
134
|
+
|
|
135
|
+
- [`SmtpError: … refused …: …`](#smtperror--refused--)
|
|
136
|
+
- [`SmtpError: … refused every recipient, … with …`](#smtperror--refused-every-recipient--with-)
|
|
137
|
+
- [`SmtpError: … sent a reply line longer than 2048 bytes`](#smtperror--sent-a-reply-line-longer-than-2048-bytes)
|
|
138
|
+
- [`SmtpError: … sent a reply of more than 100 lines`](#smtperror--sent-a-reply-of-more-than-100-lines)
|
|
139
|
+
- [`SmtpError: … sent more than 1048576 bytes of replies`](#smtperror--sent-more-than-1048576-bytes-of-replies)
|
|
140
|
+
- [`SmtpError: … sent a line that is not an SMTP reply: "…"`](#smtperror--sent-a-line-that-is-not-an-smtp-reply-)
|
|
141
|
+
- [`SmtpError: … sent a reply whose lines change code, from … to …`](#smtperror--sent-a-reply-whose-lines-change-code-from--to-)
|
|
142
|
+
- [`SmtpError: … sent more after its 220 to STARTTLS`](#smtperror--sent-more-after-its-220-to-starttls)
|
|
143
|
+
|
|
144
|
+
**Sending mail: DNS**
|
|
145
|
+
|
|
146
|
+
- [`SmtpError: … accepts no mail: its MX record is the null MX (RFC 7505)`](#smtperror--accepts-no-mail-its-mx-record-is-the-null-mx-rfc-7505)
|
|
147
|
+
- [`SmtpError: Could not look up the MX records of …: …`](#smtperror-could-not-look-up-the-mx-records-of--)
|
|
148
|
+
- [`SmtpError: No mail host of … has an address: …`](#smtperror-no-mail-host-of--has-an-address-)
|
|
149
|
+
|
|
84
150
|
## Configuration
|
|
85
151
|
|
|
86
152
|
Each of these is thrown by `createSmtpServer(options)` itself, before
|
|
@@ -265,6 +331,31 @@ createSmtpServer({
|
|
|
265
331
|
`timeout` and `hookTimeout` are in seconds: `300_000` is accepted, and
|
|
266
332
|
means 83 hours.
|
|
267
333
|
|
|
334
|
+
### `SmtpError: createSmtpServer(): hookTimeout must be at most 2147483 seconds, not …`
|
|
335
|
+
|
|
336
|
+
**When**: `hookTimeout` is past 2 147 483 seconds (about 24.8 days).
|
|
337
|
+
|
|
338
|
+
**Why**: the hook's deadline is a `setTimeout` of `hookTimeout × 1000`
|
|
339
|
+
milliseconds, and a delay past 2^31 − 1 ms fires after 1 ms instead: every
|
|
340
|
+
hook would time out at once, and every MAIL FROM, RCPT TO and message be
|
|
341
|
+
refused with `451 4.3.0`.
|
|
342
|
+
|
|
343
|
+
**Fix**: a hook that may take long still needs a bound; leave it out for
|
|
344
|
+
60 seconds, or pass a few minutes:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { createSmtpServer } from '@bumail/smtp';
|
|
348
|
+
|
|
349
|
+
createSmtpServer({
|
|
350
|
+
hostname: 'mx.example.com',
|
|
351
|
+
localDomains: ['example.com'],
|
|
352
|
+
hookTimeout: 300, // seconds
|
|
353
|
+
onData: async (message) => {
|
|
354
|
+
await new Response(message.content).bytes();
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
268
359
|
### `SmtpError: createSmtpServer(): greetingDelay must be a number of seconds, 0 or more, not …`
|
|
269
360
|
|
|
270
361
|
**When**: `greetingDelay` is negative, `NaN` or `Infinity`.
|
|
@@ -546,8 +637,9 @@ createSmtpServer({
|
|
|
546
637
|
|
|
547
638
|
### `421 4.7.0 … Too many failed authentications, closing`
|
|
548
639
|
|
|
549
|
-
**When**: the third `535 5.7.8` in one session. The server hangs up
|
|
550
|
-
|
|
640
|
+
**When**: the third `535 5.7.8` in one session. The server hangs up at
|
|
641
|
+
once: replies the client has not read are dropped, not waited for. `…` is
|
|
642
|
+
the server's `hostname`.
|
|
551
643
|
|
|
552
644
|
**Why**: it bounds password guessing on one connection. The limit is
|
|
553
645
|
three, and is not an option. A response that is not valid base64 gets
|
|
@@ -1211,8 +1303,9 @@ too, even to drop it.
|
|
|
1211
1303
|
|
|
1212
1304
|
### `421 4.3.0 Local error, closing`
|
|
1213
1305
|
|
|
1214
|
-
**When**: during a session, then the server hangs up
|
|
1215
|
-
|
|
1306
|
+
**When**: during a session, then the server hangs up at once: replies
|
|
1307
|
+
the client has not read are dropped, not waited for. The reply carries no
|
|
1308
|
+
enhanced code before EHLO.
|
|
1216
1309
|
|
|
1217
1310
|
**Why**: the server failed while handling a command, outside any hook:
|
|
1218
1311
|
a bug in this package. `onError` gets the error.
|
|
@@ -1226,7 +1319,8 @@ error, the session transcript and the `@bumail/smtp` and Bun versions.
|
|
|
1226
1319
|
### `421 4.7.0 … Too many errors, closing`
|
|
1227
1320
|
|
|
1228
1321
|
**When**: the `maxErrors`-th failed command of a session (10 by default).
|
|
1229
|
-
The reply replaces the reply to that command, and the server hangs up
|
|
1322
|
+
The reply replaces the reply to that command, and the server hangs up at
|
|
1323
|
+
once: replies the client has not read are dropped, not waited for.
|
|
1230
1324
|
|
|
1231
1325
|
**Why**: a client that keeps failing is broken or probing. What counts:
|
|
1232
1326
|
unknown commands, bad syntax, commands out of order, relay denials,
|
|
@@ -1252,7 +1346,12 @@ to retry later.
|
|
|
1252
1346
|
|
|
1253
1347
|
**Fix**, as the operator: raise `maxConnections`, or look for clients that
|
|
1254
1348
|
hold connections open without `QUIT`. `server.connections` gives the
|
|
1255
|
-
number open.
|
|
1349
|
+
number open. A client that stopped reading holds its slot until its
|
|
1350
|
+
`timeout` at most: the server's own hang-ups — idle, too many errors, a
|
|
1351
|
+
refusal — never wait for it to read, and drop what it did not. In 0.1.0
|
|
1352
|
+
such a close could wait forever, and enough of those clients filled
|
|
1353
|
+
`maxConnections`: upgrade if `connections` stays high while no client is
|
|
1354
|
+
active.
|
|
1256
1355
|
|
|
1257
1356
|
**Fix**, as a client: retry later, and send `QUIT` when done.
|
|
1258
1357
|
|
|
@@ -1266,7 +1365,11 @@ quiet, so that a dead client does not hold a connection forever. The time
|
|
|
1266
1365
|
counts from the client's last byte, or from the 220 when nothing came after
|
|
1267
1366
|
it, so a slow hook can run into it too — keep `hookTimeout` below
|
|
1268
1367
|
`timeout`. Bun's socket timer ticks in steps of about 4 seconds, so the
|
|
1269
|
-
hang-up can come up to that much after `timeout`.
|
|
1368
|
+
hang-up can come up to that much after `timeout`. The hang-up is at once
|
|
1369
|
+
even when the client stopped reading: replies it never read, and the 421
|
|
1370
|
+
itself if the socket cannot take it, are dropped, and the connection is
|
|
1371
|
+
counted out of `connections` then — on implicit TLS and after STARTTLS
|
|
1372
|
+
too, where the server does not wait for the client to answer the hang-up.
|
|
1270
1373
|
|
|
1271
1374
|
**Fix**, as a client: send `QUIT` when done; a pooled connection that
|
|
1272
1375
|
waits longer must reconnect, or send `NOOP` within the timeout.
|
|
@@ -1306,3 +1409,561 @@ client library does; a hand-written client or a test script may not.
|
|
|
1306
1409
|
**Fix**, as the operator: if a legitimate sender is refused, lower
|
|
1307
1410
|
`greetingDelay`, or leave it at `0`; a test client that writes at once
|
|
1308
1411
|
needs it at `0`.
|
|
1412
|
+
|
|
1413
|
+
## Sending mail: options
|
|
1414
|
+
|
|
1415
|
+
### `SmtpError: sendMail(): "…" is not an address (local@domain)`
|
|
1416
|
+
|
|
1417
|
+
**When**: `from` or a recipient in `to` is not an RFC 5321 path: no `@`, a
|
|
1418
|
+
display name (`Bob <b@example.org>`), angle brackets, a space, or a CR or
|
|
1419
|
+
LF. For `from` the message ends `(local@domain, or '' for a bounce)`.
|
|
1420
|
+
|
|
1421
|
+
**Why**: the address goes into `MAIL FROM:<…>` or `RCPT TO:<…>` as it is;
|
|
1422
|
+
anything else could break the command or inject another.
|
|
1423
|
+
|
|
1424
|
+
**Fix**: pass the bare address, and `''` for the null sender of a bounce:
|
|
1425
|
+
|
|
1426
|
+
```ts
|
|
1427
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1428
|
+
|
|
1429
|
+
await sendMail('Subject: hi\r\n\r\nhello\r\n', { host: 'relay.example.net', from: '', to: ['b@example.org'] });
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
### `SmtpError: sendMail(): to must name one recipient or more`
|
|
1433
|
+
|
|
1434
|
+
**When**: `to` is an empty array, or neither a string nor an array.
|
|
1435
|
+
|
|
1436
|
+
**Why**: a message with no recipient goes nowhere.
|
|
1437
|
+
|
|
1438
|
+
**Fix**: pass one address, or an array of them.
|
|
1439
|
+
|
|
1440
|
+
### `SmtpError: sendMail(): helo "…" is not a host name`
|
|
1441
|
+
|
|
1442
|
+
**When**: `helo` is not a host name nor an address literal such as
|
|
1443
|
+
`[192.0.2.1]`.
|
|
1444
|
+
|
|
1445
|
+
**Why**: it is the argument of EHLO and HELO (RFC 5321 §4.1.1.1).
|
|
1446
|
+
|
|
1447
|
+
**Fix**: pass your server's public name, the one its address resolves back
|
|
1448
|
+
to: `helo: 'mail.example.com'`.
|
|
1449
|
+
|
|
1450
|
+
### `SmtpError: sendMail(): helo is required for delivery by MX: pass your server's public name, such as helo: 'mail.example.com'`
|
|
1451
|
+
|
|
1452
|
+
**When**: `{ domain, resolver }` without `helo`. From TypeScript, `tsc`
|
|
1453
|
+
refuses it first: `MxDestination` declares `helo: string`; the error comes
|
|
1454
|
+
at runtime from JavaScript, or past a cast.
|
|
1455
|
+
|
|
1456
|
+
**Why**: by MX, the receiving host reads the EHLO name. The machine's own
|
|
1457
|
+
name — `os.hostname()`, often `laptop.local` or a bare `web-1` — is rarely
|
|
1458
|
+
a fully-qualified name that resolves back to the sending address, which
|
|
1459
|
+
receiving hosts penalise or refuse, and it tells them the machine's name.
|
|
1460
|
+
So delivery by MX has no default; to a host (`{ host }`: a smarthost,
|
|
1461
|
+
Mailpit) the machine's name stays the default.
|
|
1462
|
+
|
|
1463
|
+
**Fix**: pass the public name of the host that sends, the name its
|
|
1464
|
+
address's PTR record gives:
|
|
1465
|
+
|
|
1466
|
+
```ts
|
|
1467
|
+
import { nodeResolver } from '@bumail/dns';
|
|
1468
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1469
|
+
|
|
1470
|
+
const message = 'From: a@example.com\r\nTo: b@example.org\r\nSubject: hi\r\n\r\nhello\r\n';
|
|
1471
|
+
await sendMail(message, {
|
|
1472
|
+
domain: 'example.org',
|
|
1473
|
+
resolver: nodeResolver(),
|
|
1474
|
+
helo: 'mail.example.com',
|
|
1475
|
+
from: 'a@example.com',
|
|
1476
|
+
to: 'b@example.org',
|
|
1477
|
+
});
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
### `SmtpError: sendMail(): … must be a number of seconds above 0 and at most 2147483, not …`
|
|
1481
|
+
|
|
1482
|
+
**When**: a value in `timeouts`, or `deadline`, is `0`, negative, not a
|
|
1483
|
+
number, or past 2 147 483 seconds.
|
|
1484
|
+
|
|
1485
|
+
**Why**: each is a `setTimeout`, which fires after 1 ms for a delay past
|
|
1486
|
+
2^31 − 1 ms; and none can be switched off.
|
|
1487
|
+
|
|
1488
|
+
**Fix**: pass seconds, fractions allowed, or leave it out for its default:
|
|
1489
|
+
|
|
1490
|
+
```ts
|
|
1491
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1492
|
+
|
|
1493
|
+
await sendMail('Subject: hi\r\n\r\nhello\r\n', {
|
|
1494
|
+
host: 'relay.example.net',
|
|
1495
|
+
from: 'a@example.com', to: 'b@example.org',
|
|
1496
|
+
timeouts: { greeting: 30, dataEnd: 120 }, // seconds
|
|
1497
|
+
deadline: 300,
|
|
1498
|
+
});
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
### `SmtpError: sendMail(): tls must be 'opportunistic', 'required' or 'none', not …`
|
|
1502
|
+
|
|
1503
|
+
**When**: `tls` is anything else, such as `true`.
|
|
1504
|
+
|
|
1505
|
+
**Why**: these are the three modes; see the guide's TLS table.
|
|
1506
|
+
|
|
1507
|
+
**Fix**: `tls: 'required'` to check the certificate, `secure: true` for TLS
|
|
1508
|
+
from the first byte.
|
|
1509
|
+
|
|
1510
|
+
### `SmtpError: sendMail(): secure is TLS from the first byte: it cannot go with tls: 'none'`
|
|
1511
|
+
|
|
1512
|
+
**When**: `secure: true` and `tls: 'none'`.
|
|
1513
|
+
|
|
1514
|
+
**Why**: the two contradict each other.
|
|
1515
|
+
|
|
1516
|
+
**Fix**: leave `tls` out (`required` is then the default), or pass
|
|
1517
|
+
`'opportunistic'` to skip the certificate check (not with `auth`, unless
|
|
1518
|
+
`allowPlaintextAuth`).
|
|
1519
|
+
|
|
1520
|
+
### `SmtpError: sendMail(): auth with tls: 'none' would send the password in clear; pass allowPlaintextAuth: true for a local test server`
|
|
1521
|
+
|
|
1522
|
+
**When**: `auth` with `tls: 'none'`.
|
|
1523
|
+
|
|
1524
|
+
**Why**: the credentials would cross the network in base64.
|
|
1525
|
+
|
|
1526
|
+
**Fix**: leave `tls` out: with `auth` it defaults to `'required'`. For a
|
|
1527
|
+
local test server without TLS, such as Mailpit, pass `allowPlaintextAuth:
|
|
1528
|
+
true`.
|
|
1529
|
+
|
|
1530
|
+
### `SmtpError: sendMail(): auth with tls: 'opportunistic' would send the password to a server whose certificate is not checked; leave tls out to check it, or pass allowPlaintextAuth: true for a local test server`
|
|
1531
|
+
|
|
1532
|
+
**When**: `auth` with `tls: 'opportunistic'`, explicitly.
|
|
1533
|
+
|
|
1534
|
+
**Why**: opportunistic TLS does not check the certificate (RFC 7435), so
|
|
1535
|
+
whoever sits on the path can present any certificate and read the
|
|
1536
|
+
password. Credentials go only to a server whose certificate checked out,
|
|
1537
|
+
unless `allowPlaintextAuth` says it is a local test server.
|
|
1538
|
+
|
|
1539
|
+
**Fix**: leave `tls` out — with `auth` it defaults to `'required'` — and
|
|
1540
|
+
pass `ca` if the server's certificate is from a private CA:
|
|
1541
|
+
|
|
1542
|
+
```ts
|
|
1543
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1544
|
+
|
|
1545
|
+
const message = 'From: a@example.com\r\nTo: b@example.org\r\nSubject: hi\r\n\r\nhello\r\n';
|
|
1546
|
+
await sendMail(message, {
|
|
1547
|
+
host: 'smtp.example.com',
|
|
1548
|
+
port: 587,
|
|
1549
|
+
from: 'a@example.com',
|
|
1550
|
+
to: 'b@example.org',
|
|
1551
|
+
auth: { username: 'a', password: Bun.env['SMTP_PASSWORD'] ?? '' },
|
|
1552
|
+
ca: await Bun.file('/etc/ssl/internal-ca.pem').text(), // only for a private CA
|
|
1553
|
+
});
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
### `SmtpError: sendMail(): auth.username must be a non-empty string`
|
|
1557
|
+
|
|
1558
|
+
**When**: `auth.username` is missing, empty or not a string.
|
|
1559
|
+
|
|
1560
|
+
**Why**: PLAIN and LOGIN both need one.
|
|
1561
|
+
|
|
1562
|
+
**Fix**: pass `auth: { username, password }`, or leave `auth` out.
|
|
1563
|
+
|
|
1564
|
+
### `SmtpError: sendMail(): auth.password must be a non-empty string`
|
|
1565
|
+
|
|
1566
|
+
**When**: `auth.password` is missing, empty or not a string — often an
|
|
1567
|
+
environment variable that is not set.
|
|
1568
|
+
|
|
1569
|
+
**Why**: PLAIN and LOGIN both need one.
|
|
1570
|
+
|
|
1571
|
+
**Fix**: check the variable before sending: `Bun.env['SMTP_PASSWORD']`.
|
|
1572
|
+
|
|
1573
|
+
### `SmtpError: sendMail(): auth.mechanism must be 'PLAIN' or 'LOGIN', not …`
|
|
1574
|
+
|
|
1575
|
+
**When**: `auth.mechanism` names another SASL mechanism.
|
|
1576
|
+
|
|
1577
|
+
**Why**: the client speaks PLAIN and LOGIN only.
|
|
1578
|
+
|
|
1579
|
+
**Fix**: leave it out: PLAIN when offered, else LOGIN.
|
|
1580
|
+
|
|
1581
|
+
### `SmtpError: sendMail(): size must be a number of bytes, not …`
|
|
1582
|
+
|
|
1583
|
+
**When**: `size` is negative, a fraction or not a number.
|
|
1584
|
+
|
|
1585
|
+
**Why**: it is declared as `SIZE=` in MAIL FROM (RFC 1870).
|
|
1586
|
+
|
|
1587
|
+
**Fix**: pass the stream's length in bytes, or leave it out.
|
|
1588
|
+
|
|
1589
|
+
### `SmtpError: sendMail(): port must be an integer from 1 to 65535, not …`
|
|
1590
|
+
|
|
1591
|
+
**When**: `port` is out of range or not an integer.
|
|
1592
|
+
|
|
1593
|
+
**Why**: it is a TCP port.
|
|
1594
|
+
|
|
1595
|
+
**Fix**: 25 for MX and relay, 587 for submission, 465 with `secure: true`,
|
|
1596
|
+
1025 for Mailpit.
|
|
1597
|
+
|
|
1598
|
+
### `SmtpError: sendMail(): host must be a host name or an address, not …`
|
|
1599
|
+
|
|
1600
|
+
**When**: `host` is empty, holds a space or a `/` — often a URL such as
|
|
1601
|
+
`smtp://…`.
|
|
1602
|
+
|
|
1603
|
+
**Why**: it is what the client connects to, and what TLS checks.
|
|
1604
|
+
|
|
1605
|
+
**Fix**: pass the bare name: `host: 'smtp.example.com'`.
|
|
1606
|
+
|
|
1607
|
+
### `SmtpError: sendMail(): domain must be a domain name`
|
|
1608
|
+
|
|
1609
|
+
**When**: `domain` is empty or not a string.
|
|
1610
|
+
|
|
1611
|
+
**Why**: MX delivery looks it up.
|
|
1612
|
+
|
|
1613
|
+
**Fix**: pass the recipients' domain.
|
|
1614
|
+
|
|
1615
|
+
### `SmtpError: sendMail(): resolver must be a Resolver, such as @bumail/dns gives`
|
|
1616
|
+
|
|
1617
|
+
**When**: `{ domain }` without a `resolver`, or one without `mx`.
|
|
1618
|
+
|
|
1619
|
+
**Why**: the client asks no DNS of its own; the resolver is injected.
|
|
1620
|
+
|
|
1621
|
+
**Fix**: install `@bumail/dns` (`bun add @bumail/dns`) and pass one of
|
|
1622
|
+
its resolvers, or any object with `mx`, `a` and `aaaa` (`MxResolver`):
|
|
1623
|
+
|
|
1624
|
+
```ts
|
|
1625
|
+
import { nodeResolver } from '@bumail/dns';
|
|
1626
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1627
|
+
|
|
1628
|
+
const message = 'From: a@example.com\r\nTo: b@example.org\r\nSubject: hi\r\n\r\nhello\r\n';
|
|
1629
|
+
await sendMail(message, { domain: 'example.org', resolver: nodeResolver(), helo: 'mail.example.com', from: 'a@example.com', to: 'b@example.org' });
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
### `SmtpError: sendMail(): options must be an object`
|
|
1633
|
+
|
|
1634
|
+
**When**: `options` is missing or `null`.
|
|
1635
|
+
|
|
1636
|
+
**Why**: it holds `from`, `to` and the destination.
|
|
1637
|
+
|
|
1638
|
+
**Fix**: `sendMail(message, { host, from, to })`.
|
|
1639
|
+
|
|
1640
|
+
### `SmtpError: sendMail(): the message must be a Uint8Array, a string or a ReadableStream`
|
|
1641
|
+
|
|
1642
|
+
**When**: the message is another type: an object, a `Blob`, a `Response`.
|
|
1643
|
+
|
|
1644
|
+
**Why**: those are the three forms read.
|
|
1645
|
+
|
|
1646
|
+
**Fix**: `await blob.bytes()`, `blob.stream()` or `response.body`.
|
|
1647
|
+
|
|
1648
|
+
### `SmtpError: sendMail(): a message stream must give Uint8Array chunks`
|
|
1649
|
+
|
|
1650
|
+
**When**: a stream gave a string or another value; the client hung up before
|
|
1651
|
+
the final dot.
|
|
1652
|
+
|
|
1653
|
+
**Why**: the bytes are sent as they are.
|
|
1654
|
+
|
|
1655
|
+
**Fix**: pipe it through a `TextEncoderStream`, or give bytes.
|
|
1656
|
+
|
|
1657
|
+
### `SmtpError: sendMail(): a message stream can be sent once only`
|
|
1658
|
+
|
|
1659
|
+
**When**: the same `ReadableStream` was read twice.
|
|
1660
|
+
|
|
1661
|
+
**Why**: a stream is consumed as it is sent.
|
|
1662
|
+
|
|
1663
|
+
**Fix**: make a new stream per `sendMail`, or pass bytes.
|
|
1664
|
+
|
|
1665
|
+
## Sending mail: the message
|
|
1666
|
+
|
|
1667
|
+
### `SmtpError: sendMail(): the message holds a bare CR or LF, which a server must refuse (SMTP smuggling); end every line with CRLF, or pass normalizeLineEnds: true`
|
|
1668
|
+
|
|
1669
|
+
**When**: a line of the message ends with LF alone (or CR alone): text
|
|
1670
|
+
written with `\n`, a file saved on Unix. For a string or bytes, before
|
|
1671
|
+
connecting; for a stream, while sending — the client then hangs up before
|
|
1672
|
+
the dot, and nothing is delivered.
|
|
1673
|
+
|
|
1674
|
+
**Why**: a server that read a bare LF as a line end could be made to see two
|
|
1675
|
+
messages in one (SMTP smuggling); this package's server refuses such a
|
|
1676
|
+
message, as it should.
|
|
1677
|
+
|
|
1678
|
+
**Fix**: end every line with `\r\n`, or let the client do it:
|
|
1679
|
+
|
|
1680
|
+
```ts
|
|
1681
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1682
|
+
|
|
1683
|
+
await sendMail('Subject: hi\n\nhello\n', { host: 'relay.example.net', from: 'a@example.com', to: 'b@example.org', normalizeLineEnds: true });
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
### `SmtpError: The message (… bytes) is larger than the … bytes … takes (SIZE)`
|
|
1687
|
+
|
|
1688
|
+
**When**: the server announced a SIZE smaller than the message. Nothing was
|
|
1689
|
+
sent past EHLO.
|
|
1690
|
+
|
|
1691
|
+
**Why**: RFC 1870: the server would refuse it with `552` anyway.
|
|
1692
|
+
|
|
1693
|
+
**Fix**: send a smaller message — a link rather than an attachment. It is
|
|
1694
|
+
permanent: do not retry.
|
|
1695
|
+
|
|
1696
|
+
### `SmtpError: … does not offer 8BITMIME, which the message needs: it has 8-bit bytes`
|
|
1697
|
+
|
|
1698
|
+
**When**: the message holds a byte above 127 (UTF-8 text, a raw attachment)
|
|
1699
|
+
and the server does not offer 8BITMIME. For a stream, the client hangs up
|
|
1700
|
+
before the dot.
|
|
1701
|
+
|
|
1702
|
+
**Why**: a server without 8BITMIME may strip the eighth bit.
|
|
1703
|
+
|
|
1704
|
+
**Fix**: encode the body: quoted-printable or base64 (`@bumail/mime` writes
|
|
1705
|
+
both), so the message is 7-bit.
|
|
1706
|
+
|
|
1707
|
+
### `SmtpError: … does not offer SMTPUTF8, which an address that is not ASCII needs (or smtputf8 asked for)`
|
|
1708
|
+
|
|
1709
|
+
**When**: an address in `from` or `to` is not ASCII (`zoë@example.org`), or
|
|
1710
|
+
`smtputf8: true`, and the server does not offer SMTPUTF8.
|
|
1711
|
+
|
|
1712
|
+
**Why**: RFC 6531 §3.2: such a message cannot be sent to it.
|
|
1713
|
+
|
|
1714
|
+
**Fix**: use an ASCII address if the user has one; else it is permanent for
|
|
1715
|
+
that server.
|
|
1716
|
+
|
|
1717
|
+
## Sending mail: connections, TLS and AUTH
|
|
1718
|
+
|
|
1719
|
+
### `SmtpError: Could not connect to … (…:…): …`
|
|
1720
|
+
|
|
1721
|
+
**When**: the TCP connection failed: `ECONNREFUSED` (nothing listens there:
|
|
1722
|
+
Mailpit not started, the wrong port), `ENOTFOUND`, a firewall. Temporary.
|
|
1723
|
+
|
|
1724
|
+
**Fix**: check the server is up and the port; many networks block outbound
|
|
1725
|
+
port 25 — use a smarthost on 587 then.
|
|
1726
|
+
|
|
1727
|
+
### `SmtpError: Timed out after … s waiting for … (…)`
|
|
1728
|
+
|
|
1729
|
+
**When**: a step took longer than its timeout: `the connection`, `the
|
|
1730
|
+
greeting`, `the reply to MAIL FROM`, `the server to take the message`…
|
|
1731
|
+
Temporary. `the next part of the message` is your own stream: it gave
|
|
1732
|
+
nothing for `timeouts.dataBlock` seconds, and the client hung up before the
|
|
1733
|
+
dot.
|
|
1734
|
+
|
|
1735
|
+
**Why**: every wait is bounded, so a slow or silent server cannot hold the
|
|
1736
|
+
delivery.
|
|
1737
|
+
|
|
1738
|
+
**Fix**: retry later; raise that step in `timeouts` if the server is known
|
|
1739
|
+
to be slow, such as a greylisting MX that delays its greeting.
|
|
1740
|
+
|
|
1741
|
+
### `SmtpError: The deadline of … s passed waiting for … (…)`
|
|
1742
|
+
|
|
1743
|
+
**When**: the whole delivery, every MX host and DNS lookup together, took
|
|
1744
|
+
longer than `deadline`. Temporary. By MX, `the MX lookup (example.org)` or
|
|
1745
|
+
`the address lookup (mx1.example.org)` is a resolver that answered too late.
|
|
1746
|
+
|
|
1747
|
+
**Why**: one bound on the delivery, whatever the steps.
|
|
1748
|
+
|
|
1749
|
+
**Fix**: retry later, or raise `deadline` (1800 s by default).
|
|
1750
|
+
|
|
1751
|
+
### `SmtpError: The connection to … closed before …`
|
|
1752
|
+
|
|
1753
|
+
**When**: the server hung up while the client waited for a reply. Temporary.
|
|
1754
|
+
|
|
1755
|
+
**Why**: a server that drops a session it will not serve, or crashed.
|
|
1756
|
+
|
|
1757
|
+
**Fix**: retry later. If it happens right after EHLO, look at the server's
|
|
1758
|
+
log: it may refuse the client's `helo` or address.
|
|
1759
|
+
|
|
1760
|
+
### `SmtpError: The connection to … closed while the message was sent`
|
|
1761
|
+
|
|
1762
|
+
**When**: the server hung up during DATA. Temporary; the message was not
|
|
1763
|
+
delivered.
|
|
1764
|
+
|
|
1765
|
+
**Why**: often a size limit the server did not announce, or a time limit.
|
|
1766
|
+
|
|
1767
|
+
**Fix**: retry later; check the server's limits.
|
|
1768
|
+
|
|
1769
|
+
### `SmtpError: TLS with … failed: …`
|
|
1770
|
+
|
|
1771
|
+
**When**: the TLS handshake failed. With `tls: 'required'` or `secure:
|
|
1772
|
+
true`, the reason is the certificate's: `DEPTH_ZERO_SELF_SIGNED_CERT`,
|
|
1773
|
+
`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `ERR_TLS_CERT_ALTNAME_INVALID` (a
|
|
1774
|
+
certificate for another name). Temporary.
|
|
1775
|
+
|
|
1776
|
+
**Why**: a certificate that does not check out against the host name may be
|
|
1777
|
+
an attacker's.
|
|
1778
|
+
|
|
1779
|
+
**Fix**: connect by the name the certificate is for; trust a private CA or a
|
|
1780
|
+
test's self-signed certificate with `ca`:
|
|
1781
|
+
|
|
1782
|
+
```ts
|
|
1783
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1784
|
+
|
|
1785
|
+
await sendMail('Subject: hi\r\n\r\nhello\r\n', {
|
|
1786
|
+
host: 'mail.internal.example',
|
|
1787
|
+
port: 587,
|
|
1788
|
+
tls: 'required',
|
|
1789
|
+
ca: await Bun.file('/etc/ssl/internal-ca.pem').text(),
|
|
1790
|
+
from: 'a@example.com', to: 'b@example.org',
|
|
1791
|
+
});
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
### `SmtpError: … does not offer STARTTLS, and tls is 'required'`
|
|
1795
|
+
|
|
1796
|
+
**When**: `tls: 'required'` — the default with `auth` — and the server's
|
|
1797
|
+
EHLO reply has no STARTTLS. Temporary.
|
|
1798
|
+
|
|
1799
|
+
**Why**: credentials, and a message that must be encrypted, are not sent in
|
|
1800
|
+
clear.
|
|
1801
|
+
|
|
1802
|
+
**Fix**: use the server's submission port (587, or 465 with `secure: true`).
|
|
1803
|
+
For a local test server without TLS, pass `allowPlaintextAuth: true`, or
|
|
1804
|
+
`tls: 'opportunistic'` without `auth`.
|
|
1805
|
+
|
|
1806
|
+
### `SmtpError: … refused STARTTLS, and tls is 'required': …`
|
|
1807
|
+
|
|
1808
|
+
**When**: the server offers STARTTLS but answered it with another reply than
|
|
1809
|
+
220, such as `454 4.7.0 TLS not available`. Temporary.
|
|
1810
|
+
|
|
1811
|
+
**Fix**: retry later; the server's TLS is broken.
|
|
1812
|
+
|
|
1813
|
+
### `SmtpError: Refusing to send credentials to … in clear: it did not start TLS (allowPlaintextAuth is for a local test server only)`
|
|
1814
|
+
|
|
1815
|
+
**When**: `auth` and the session is not encrypted. The option checks
|
|
1816
|
+
already refuse every combination that leads here — `auth` with `tls:
|
|
1817
|
+
'none'` or `'opportunistic'` needs `allowPlaintextAuth`, and `'required'`
|
|
1818
|
+
fails without TLS — so this is the last guard before the password is
|
|
1819
|
+
written, and should not be seen. Permanent.
|
|
1820
|
+
|
|
1821
|
+
**Why**: the password would cross the network in base64.
|
|
1822
|
+
|
|
1823
|
+
**Fix**: use TLS. For a local test server only, such as Mailpit started with
|
|
1824
|
+
`--smtp-auth-accept-any --smtp-auth-allow-insecure`:
|
|
1825
|
+
|
|
1826
|
+
```ts
|
|
1827
|
+
import { sendMail } from '@bumail/smtp/client';
|
|
1828
|
+
|
|
1829
|
+
await sendMail('Subject: hi\r\n\r\nhello\r\n', {
|
|
1830
|
+
host: 'localhost',
|
|
1831
|
+
port: 1025,
|
|
1832
|
+
from: 'a@example.com', to: 'b@example.org',
|
|
1833
|
+
auth: { username: 'test', password: 'test' },
|
|
1834
|
+
allowPlaintextAuth: true,
|
|
1835
|
+
});
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1838
|
+
### `SmtpError: … offers no AUTH mechanism this client speaks (…): AUTH …`
|
|
1839
|
+
|
|
1840
|
+
**When**: the server offers no AUTH, or only mechanisms other than PLAIN and
|
|
1841
|
+
LOGIN (or not the `auth.mechanism` asked). Permanent.
|
|
1842
|
+
|
|
1843
|
+
**Fix**: leave `auth.mechanism` out; if AUTH is missing, the server may
|
|
1844
|
+
offer it only on another port, or only after TLS.
|
|
1845
|
+
|
|
1846
|
+
## Sending mail: replies
|
|
1847
|
+
|
|
1848
|
+
### `SmtpError: … refused …: …`
|
|
1849
|
+
|
|
1850
|
+
**When**: the server answered the connection, EHLO, HELO, `AUTH PLAIN` or
|
|
1851
|
+
`AUTH LOGIN`, the sender (MAIL FROM), DATA, or the message (the final dot)
|
|
1852
|
+
with a 4xx or 5xx; the reply follows the colon, and is in `error.reply`. A
|
|
1853
|
+
4xx is temporary, a 5xx permanent.
|
|
1854
|
+
|
|
1855
|
+
**Why**: the server's policy: `535 5.7.8` bad credentials, `550 5.7.1` a
|
|
1856
|
+
sender it refuses, `421` busy, `554` a spam verdict.
|
|
1857
|
+
|
|
1858
|
+
**Fix**: read the reply and its enhanced status code; this package's own
|
|
1859
|
+
replies are explained above. Retry a 4xx later; do not retry a 5xx.
|
|
1860
|
+
|
|
1861
|
+
### `SmtpError: … refused every recipient, … with …`
|
|
1862
|
+
|
|
1863
|
+
**When**: every RCPT TO got another reply than 250 or 251; the first is in
|
|
1864
|
+
the message, all of them in `error.rejected`. Temporary if one of them was a
|
|
1865
|
+
4xx.
|
|
1866
|
+
|
|
1867
|
+
**Why**: no recipient, no message: DATA is not sent.
|
|
1868
|
+
|
|
1869
|
+
**Fix**: check the addresses; retry the 4xx ones later. When only some are
|
|
1870
|
+
refused, `sendMail` resolves and lists them in `result.rejected`.
|
|
1871
|
+
|
|
1872
|
+
### `SmtpError: … sent a reply line longer than 2048 bytes`
|
|
1873
|
+
|
|
1874
|
+
**When**: a line from the server, before its CRLF, is longer than 2048
|
|
1875
|
+
bytes. Temporary.
|
|
1876
|
+
|
|
1877
|
+
**Why**: RFC 5321 §4.5.3.1.5 bounds a reply line at 512; the client allows
|
|
1878
|
+
four times that, and no more, so a server cannot make it buffer without end.
|
|
1879
|
+
|
|
1880
|
+
**Fix**: none on your side; the server is broken or hostile. It is
|
|
1881
|
+
temporary: by MX, the next host is tried when it came before MAIL
|
|
1882
|
+
FROM.
|
|
1883
|
+
|
|
1884
|
+
### `SmtpError: … sent a reply of more than 100 lines`
|
|
1885
|
+
|
|
1886
|
+
**When**: a multiline reply (`250-…`) went past 100 lines. Temporary.
|
|
1887
|
+
|
|
1888
|
+
**Why**: an EHLO reply has a dozen; a reply that never ends would hold the
|
|
1889
|
+
client.
|
|
1890
|
+
|
|
1891
|
+
**Fix**: none on your side; the server is broken or hostile. It is
|
|
1892
|
+
temporary: by MX, the next host is tried when it came before MAIL
|
|
1893
|
+
FROM.
|
|
1894
|
+
|
|
1895
|
+
### `SmtpError: … sent more than 1048576 bytes of replies`
|
|
1896
|
+
|
|
1897
|
+
**When**: the server sent more than 1 MiB in one session, every reply
|
|
1898
|
+
together. Temporary.
|
|
1899
|
+
|
|
1900
|
+
**Why**: a bound on what a server can make the client hold.
|
|
1901
|
+
|
|
1902
|
+
**Fix**: none on your side; the server is broken or hostile. It is
|
|
1903
|
+
temporary: by MX, the next host is tried when it came before MAIL
|
|
1904
|
+
FROM.
|
|
1905
|
+
|
|
1906
|
+
### `SmtpError: … sent a line that is not an SMTP reply: "…"`
|
|
1907
|
+
|
|
1908
|
+
**When**: a line does not start with three digits (2 to 5 first) and a
|
|
1909
|
+
space, a hyphen or its end: the port is not SMTP (an HTTP server, a TLS port
|
|
1910
|
+
spoken to in clear), or a proxy answered. Temporary.
|
|
1911
|
+
|
|
1912
|
+
**Why**: the client never guesses at what a reply means.
|
|
1913
|
+
|
|
1914
|
+
**Fix**: check the port, and `secure: true` for 465.
|
|
1915
|
+
|
|
1916
|
+
### `SmtpError: … sent a reply whose lines change code, from … to …`
|
|
1917
|
+
|
|
1918
|
+
**When**: the lines of one multiline reply carry different codes. Temporary.
|
|
1919
|
+
|
|
1920
|
+
**Why**: RFC 5321 §4.2.1: every line of a reply has the same code.
|
|
1921
|
+
|
|
1922
|
+
**Fix**: none on your side; the server is broken. It is temporary: by MX,
|
|
1923
|
+
the next host is tried when it came before MAIL
|
|
1924
|
+
FROM.
|
|
1925
|
+
|
|
1926
|
+
### `SmtpError: … sent more after its 220 to STARTTLS`
|
|
1927
|
+
|
|
1928
|
+
**When**: the server sent more after its `220` to STARTTLS, before the TLS
|
|
1929
|
+
handshake. Temporary.
|
|
1930
|
+
|
|
1931
|
+
**Why**: those bytes would be read as if they came over TLS: a man in the
|
|
1932
|
+
middle can inject replies there (CVE-2011-0411).
|
|
1933
|
+
|
|
1934
|
+
**Fix**: none on your side; the path to the server is not to be trusted. It
|
|
1935
|
+
is temporary: by MX, the next host is tried when it came before MAIL
|
|
1936
|
+
FROM.
|
|
1937
|
+
|
|
1938
|
+
## Sending mail: DNS
|
|
1939
|
+
|
|
1940
|
+
### `SmtpError: … accepts no mail: its MX record is the null MX (RFC 7505)`
|
|
1941
|
+
|
|
1942
|
+
**When**: the domain publishes `MX 0 .`. Permanent.
|
|
1943
|
+
|
|
1944
|
+
**Why**: RFC 7505: the domain says it takes no mail; a sender gives up at
|
|
1945
|
+
once, with no fallback to its address.
|
|
1946
|
+
|
|
1947
|
+
**Fix**: bounce the message: the address cannot receive mail.
|
|
1948
|
+
|
|
1949
|
+
### `SmtpError: Could not look up the MX records of …: …`
|
|
1950
|
+
|
|
1951
|
+
**When**: the resolver failed for the MX query: `TEMPORARY`, `TIMEOUT`
|
|
1952
|
+
(temporary), or `INVALID_NAME` (permanent).
|
|
1953
|
+
|
|
1954
|
+
**Why**: the client tries no host it cannot find.
|
|
1955
|
+
|
|
1956
|
+
**Fix**: retry later; for `INVALID_NAME`, the recipient's domain is wrong.
|
|
1957
|
+
|
|
1958
|
+
### `SmtpError: No mail host of … has an address: …`
|
|
1959
|
+
|
|
1960
|
+
**When**: neither the MX hosts nor (with no MX) the domain itself have an A
|
|
1961
|
+
or AAAA record. Permanent when the DNS said so — or the resolver answered
|
|
1962
|
+
with no record, and the message ends `no A or AAAA record` — temporary
|
|
1963
|
+
when a lookup failed.
|
|
1964
|
+
|
|
1965
|
+
**Why**: RFC 5321 §5.1: a domain with no MX is reached at its own address,
|
|
1966
|
+
and this one has none.
|
|
1967
|
+
|
|
1968
|
+
**Fix**: check the domain; a typo in the recipient's domain gives this,
|
|
1969
|
+
permanently.
|