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