@upyo/smtp 0.6.0-dev.298 → 0.6.0-dev.301
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 +35 -0
- package/dist/index.cjs +112 -21
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +111 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Features
|
|
|
28
28
|
- Multiple recipients (To, CC, BCC)
|
|
29
29
|
- SMTP PIPELINING for faster multi-recipient delivery
|
|
30
30
|
- SMTP SIZE declaration and advertised-limit checks
|
|
31
|
+
- Structured SMTP enhanced status codes
|
|
31
32
|
- SMTPUTF8 internationalized address delivery
|
|
32
33
|
- SMTP delivery status notification requests
|
|
33
34
|
- Custom headers
|
|
@@ -184,6 +185,40 @@ See [RFC 1870] for the SMTP Message Size Declaration extension.
|
|
|
184
185
|
[RFC 1870]: https://www.rfc-editor.org/rfc/rfc1870
|
|
185
186
|
|
|
186
187
|
|
|
188
|
+
Enhanced status codes
|
|
189
|
+
---------------------
|
|
190
|
+
|
|
191
|
+
SMTP failures include a parsed enhanced status code when the server prefixes
|
|
192
|
+
its reply text with a valid RFC 2034 code such as `5.1.1`. The final reply
|
|
193
|
+
line's text remains in `providerDetails.response`, while
|
|
194
|
+
`providerDetails.enhancedStatusCode` provides the complete code and its numeric
|
|
195
|
+
`class`, `subject`, and `detail` fields. Address and message-content statuses
|
|
196
|
+
are categorized as validation failures, and network/routing statuses as network
|
|
197
|
+
failures.
|
|
198
|
+
|
|
199
|
+
Use `isSmtpResponseProviderDetails()` to narrow the provider-specific details:
|
|
200
|
+
|
|
201
|
+
~~~~ typescript
|
|
202
|
+
import { isSmtpResponseProviderDetails } from "@upyo/smtp";
|
|
203
|
+
|
|
204
|
+
const error = receipt.successful ? undefined : receipt.errors?.[0];
|
|
205
|
+
if (isSmtpResponseProviderDetails(error?.providerDetails)) {
|
|
206
|
+
console.log(error.providerDetails.enhancedStatusCode?.subject);
|
|
207
|
+
}
|
|
208
|
+
~~~~
|
|
209
|
+
|
|
210
|
+
Partially rejected recipients expose the same structure through
|
|
211
|
+
`receipt.rejectedRecipients[].enhancedStatusCode`. Replies without an enhanced
|
|
212
|
+
code, with malformed fields, or with a class that conflicts with the
|
|
213
|
+
three-digit SMTP reply continue to use the traditional reply classification.
|
|
214
|
+
|
|
215
|
+
See [RFC 2034] for the SMTP extension and [RFC 3463] for the status-code
|
|
216
|
+
structure.
|
|
217
|
+
|
|
218
|
+
[RFC 2034]: https://www.rfc-editor.org/rfc/rfc2034
|
|
219
|
+
[RFC 3463]: https://www.rfc-editor.org/rfc/rfc3463
|
|
220
|
+
|
|
221
|
+
|
|
187
222
|
Internationalized addresses
|
|
188
223
|
---------------------------
|
|
189
224
|
|
package/dist/index.cjs
CHANGED
|
@@ -472,6 +472,34 @@ var OAuth2TokenManager = class {
|
|
|
472
472
|
}
|
|
473
473
|
};
|
|
474
474
|
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/smtp-status-code.ts
|
|
477
|
+
/**
|
|
478
|
+
* Parses an enhanced SMTP status code from the beginning of reply text.
|
|
479
|
+
*
|
|
480
|
+
* RFC 2034 requires the enhanced class to agree with the three-digit SMTP
|
|
481
|
+
* reply class. RFC 3463 also prohibits leading zeroes and limits the subject
|
|
482
|
+
* and detail fields to three digits.
|
|
483
|
+
*
|
|
484
|
+
* @param replyCode The three-digit SMTP reply code.
|
|
485
|
+
* @param response The textual part of the SMTP reply.
|
|
486
|
+
* @returns The parsed enhanced status code, or `undefined` when the reply does
|
|
487
|
+
* not begin with a valid, consistent code.
|
|
488
|
+
* @since 0.6.0
|
|
489
|
+
*/
|
|
490
|
+
function parseEnhancedSmtpStatusCode(replyCode, response) {
|
|
491
|
+
const match = /^([245])\.((?:0|[1-9][0-9]{0,2}))\.((?:0|[1-9][0-9]{0,2})) +/.exec(response);
|
|
492
|
+
if (match == null) return void 0;
|
|
493
|
+
const statusClass = Number(match[1]);
|
|
494
|
+
if (Math.trunc(replyCode / 100) !== statusClass) return void 0;
|
|
495
|
+
return {
|
|
496
|
+
code: `${match[1]}.${match[2]}.${match[3]}`,
|
|
497
|
+
class: statusClass,
|
|
498
|
+
subject: Number(match[2]),
|
|
499
|
+
detail: Number(match[3])
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
475
503
|
//#endregion
|
|
476
504
|
//#region src/smtp-connection.ts
|
|
477
505
|
/**
|
|
@@ -535,6 +563,37 @@ var SmtpPipelineTerminatedError = class extends Error {
|
|
|
535
563
|
}
|
|
536
564
|
};
|
|
537
565
|
/**
|
|
566
|
+
* An authentication failure backed by an SMTP server reply.
|
|
567
|
+
*
|
|
568
|
+
* This keeps {@link SmtpAuthError} as the public authentication error while
|
|
569
|
+
* retaining the reply fields needed to build structured transport receipts.
|
|
570
|
+
*
|
|
571
|
+
* @since 0.6.0
|
|
572
|
+
*/
|
|
573
|
+
var SmtpAuthResponseError = class extends SmtpAuthError {
|
|
574
|
+
/** The numeric SMTP reply code returned by the server. */
|
|
575
|
+
code;
|
|
576
|
+
/** The authentication command that produced the reply. */
|
|
577
|
+
command;
|
|
578
|
+
/** The textual SMTP reply returned by the server. */
|
|
579
|
+
response;
|
|
580
|
+
/**
|
|
581
|
+
* Creates an authentication response error.
|
|
582
|
+
*
|
|
583
|
+
* @param message A human-readable description of the authentication failure.
|
|
584
|
+
* @param code The numeric SMTP reply code.
|
|
585
|
+
* @param command The authentication command that produced the reply.
|
|
586
|
+
* @param response The textual SMTP reply returned by the server.
|
|
587
|
+
*/
|
|
588
|
+
constructor(message, code, command, response) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.name = "SmtpAuthResponseError";
|
|
591
|
+
this.code = code;
|
|
592
|
+
this.command = command;
|
|
593
|
+
this.response = response;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
538
597
|
* The maximum length of an SMTP command line, including the terminating CRLF,
|
|
539
598
|
* as specified by RFC 5321 §4.5.3.1.4.
|
|
540
599
|
*/
|
|
@@ -923,16 +982,16 @@ var SmtpConnection = class {
|
|
|
923
982
|
const { user, pass } = auth;
|
|
924
983
|
const credentials = btoa(`\0${user}\0${pass}`);
|
|
925
984
|
const response = await this.sendCommand(`AUTH PLAIN ${credentials}`, signal);
|
|
926
|
-
if (response.code !== 235) throw new
|
|
985
|
+
if (response.code !== 235) throw new SmtpAuthResponseError(`Authentication failed: ${response.message}`, response.code, "AUTH PLAIN", response.message);
|
|
927
986
|
}
|
|
928
987
|
async authLogin(auth, signal) {
|
|
929
988
|
const { user, pass } = auth;
|
|
930
989
|
let response = await this.sendCommand("AUTH LOGIN", signal);
|
|
931
|
-
if (response.code !== 334) throw new
|
|
990
|
+
if (response.code !== 334) throw new SmtpAuthResponseError(`AUTH LOGIN failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
932
991
|
response = await this.sendCommand(btoa(user), signal);
|
|
933
|
-
if (response.code !== 334) throw new
|
|
992
|
+
if (response.code !== 334) throw new SmtpAuthResponseError(`Username authentication failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
934
993
|
response = await this.sendCommand(btoa(pass), signal);
|
|
935
|
-
if (response.code !== 235) throw new
|
|
994
|
+
if (response.code !== 235) throw new SmtpAuthResponseError(`Password authentication failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
936
995
|
}
|
|
937
996
|
/**
|
|
938
997
|
* Resolves an OAuth 2.0 access token via the connection's token manager,
|
|
@@ -984,16 +1043,19 @@ var SmtpConnection = class {
|
|
|
984
1043
|
async finishOAuth2(response, mechanism, continuation, signal) {
|
|
985
1044
|
if (response.code === 235) return;
|
|
986
1045
|
if (response.code === 334) {
|
|
1046
|
+
let finalResponse;
|
|
987
1047
|
let finalMessage = "";
|
|
988
1048
|
try {
|
|
989
|
-
|
|
990
|
-
finalMessage = ` (${
|
|
1049
|
+
finalResponse = await this.sendCommand(continuation, signal);
|
|
1050
|
+
finalMessage = ` (${finalResponse.message})`;
|
|
991
1051
|
} catch {
|
|
992
1052
|
signal?.throwIfAborted();
|
|
993
1053
|
}
|
|
994
|
-
|
|
1054
|
+
const message = `${mechanism} authentication failed: ${decodeOAuth2Challenge(response.message)}${finalMessage}`;
|
|
1055
|
+
if (finalResponse != null) throw new SmtpAuthResponseError(message, finalResponse.code, `AUTH ${mechanism}`, finalResponse.message);
|
|
1056
|
+
throw new SmtpAuthError(message);
|
|
995
1057
|
}
|
|
996
|
-
throw new
|
|
1058
|
+
throw new SmtpAuthResponseError(`${mechanism} authentication failed: ${response.message}`, response.code, `AUTH ${mechanism}`, response.message);
|
|
997
1059
|
}
|
|
998
1060
|
async sendMessage(message, signal) {
|
|
999
1061
|
signal?.throwIfAborted();
|
|
@@ -1043,12 +1105,16 @@ var SmtpConnection = class {
|
|
|
1043
1105
|
signal?.throwIfAborted();
|
|
1044
1106
|
const rcptResponse = pipelining ? recipientResponses[index] : await this.sendCommand(recipientCommands[index], signal);
|
|
1045
1107
|
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
1046
|
-
if (rcptResponse.code !== 250 && rcptResponse.code !== 251)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1108
|
+
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) {
|
|
1109
|
+
const enhancedStatusCode = parseEnhancedSmtpStatusCode(rcptResponse.code, rcptResponse.message);
|
|
1110
|
+
rejectedRecipients.push({
|
|
1111
|
+
recipient,
|
|
1112
|
+
code: rcptResponse.code,
|
|
1113
|
+
response: rcptResponse.message,
|
|
1114
|
+
retryable: rcptResponse.code >= 400 && rcptResponse.code < 500,
|
|
1115
|
+
...enhancedStatusCode == null ? {} : { enhancedStatusCode }
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1052
1118
|
}
|
|
1053
1119
|
if (rejectedRecipients.length > 0 && rejectedRecipients.length === message.envelope.to.length) {
|
|
1054
1120
|
const rejection = rejectedRecipients.find((item) => item.retryable) ?? rejectedRecipients[0];
|
|
@@ -2017,8 +2083,9 @@ function createSmtpFailure(message, error) {
|
|
|
2017
2083
|
attempts: 1,
|
|
2018
2084
|
providerDetails: { missingCapability: error.missingCapability }
|
|
2019
2085
|
});
|
|
2020
|
-
if (error instanceof SmtpResponseError) {
|
|
2021
|
-
const
|
|
2086
|
+
if (error instanceof SmtpResponseError || error instanceof SmtpAuthResponseError) {
|
|
2087
|
+
const enhancedStatusCode = parseEnhancedSmtpStatusCode(error.code, error.response);
|
|
2088
|
+
const classification = classifySmtpReply(error.code, enhancedStatusCode);
|
|
2022
2089
|
return (0, __upyo_core.createFailedReceipt)(message, {
|
|
2023
2090
|
provider: "smtp",
|
|
2024
2091
|
code: `smtp.${error.code}`,
|
|
@@ -2028,7 +2095,8 @@ function createSmtpFailure(message, error) {
|
|
|
2028
2095
|
providerDetails: {
|
|
2029
2096
|
command: error.command,
|
|
2030
2097
|
response: error.response,
|
|
2031
|
-
rejectedRecipients: error.rejectedRecipients
|
|
2098
|
+
rejectedRecipients: error instanceof SmtpResponseError ? error.rejectedRecipients : void 0,
|
|
2099
|
+
...enhancedStatusCode == null ? {} : { enhancedStatusCode }
|
|
2032
2100
|
}
|
|
2033
2101
|
});
|
|
2034
2102
|
}
|
|
@@ -2040,14 +2108,23 @@ function createSmtpFailure(message, error) {
|
|
|
2040
2108
|
function isReusableLocalFailure(error) {
|
|
2041
2109
|
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2042
2110
|
}
|
|
2043
|
-
function classifySmtpReply(code) {
|
|
2111
|
+
function classifySmtpReply(code, enhancedStatusCode) {
|
|
2112
|
+
const retryable = enhancedStatusCode == null ? code >= 400 && code < 500 : enhancedStatusCode.class === 4;
|
|
2113
|
+
if (enhancedStatusCode?.subject === 1 || enhancedStatusCode?.subject === 6) return {
|
|
2114
|
+
category: "validation",
|
|
2115
|
+
retryable
|
|
2116
|
+
};
|
|
2117
|
+
if (enhancedStatusCode?.subject === 4) return {
|
|
2118
|
+
category: "network",
|
|
2119
|
+
retryable
|
|
2120
|
+
};
|
|
2044
2121
|
if (code >= 400 && code < 500) return {
|
|
2045
2122
|
category: "service-unavailable",
|
|
2046
|
-
retryable
|
|
2123
|
+
retryable
|
|
2047
2124
|
};
|
|
2048
2125
|
if (code >= 500 && code < 600) return {
|
|
2049
2126
|
category: "rejected",
|
|
2050
|
-
retryable
|
|
2127
|
+
retryable
|
|
2051
2128
|
};
|
|
2052
2129
|
return {
|
|
2053
2130
|
category: "unknown",
|
|
@@ -2055,8 +2132,22 @@ function classifySmtpReply(code) {
|
|
|
2055
2132
|
};
|
|
2056
2133
|
}
|
|
2057
2134
|
|
|
2135
|
+
//#endregion
|
|
2136
|
+
//#region src/smtp-receipt.ts
|
|
2137
|
+
/**
|
|
2138
|
+
* Checks whether provider details came from an SMTP server response.
|
|
2139
|
+
*
|
|
2140
|
+
* @param value Provider details from an SMTP receipt error.
|
|
2141
|
+
* @returns Whether the value contains SMTP response details.
|
|
2142
|
+
* @since 0.6.0
|
|
2143
|
+
*/
|
|
2144
|
+
function isSmtpResponseProviderDetails(value) {
|
|
2145
|
+
return value != null && typeof value === "object" && "command" in value && typeof value.command === "string" && "response" in value && typeof value.response === "string";
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2058
2148
|
//#endregion
|
|
2059
2149
|
exports.SmtpAuthError = SmtpAuthError;
|
|
2060
2150
|
exports.SmtpDsnUnsupportedError = SmtpDsnUnsupportedError;
|
|
2061
2151
|
exports.SmtpDsnValidationError = SmtpDsnValidationError;
|
|
2062
|
-
exports.SmtpTransport = SmtpTransport;
|
|
2152
|
+
exports.SmtpTransport = SmtpTransport;
|
|
2153
|
+
exports.isSmtpResponseProviderDetails = isSmtpResponseProviderDetails;
|
package/dist/index.d.cts
CHANGED
|
@@ -510,6 +510,21 @@ declare class SmtpDsnUnsupportedError extends Error {
|
|
|
510
510
|
/** @internal */
|
|
511
511
|
//#endregion
|
|
512
512
|
//#region src/smtp-receipt.d.ts
|
|
513
|
+
/**
|
|
514
|
+
* A machine-readable enhanced SMTP status code defined by RFC 3463.
|
|
515
|
+
*
|
|
516
|
+
* @since 0.6.0
|
|
517
|
+
*/
|
|
518
|
+
interface SmtpEnhancedStatusCode {
|
|
519
|
+
/** The complete enhanced status code. */
|
|
520
|
+
readonly code: string;
|
|
521
|
+
/** The delivery result class: success, transient failure, or permanent failure. */
|
|
522
|
+
readonly class: 2 | 4 | 5;
|
|
523
|
+
/** The subject identifying the probable source of the condition. */
|
|
524
|
+
readonly subject: number;
|
|
525
|
+
/** The detail identifying the precise condition. */
|
|
526
|
+
readonly detail: number;
|
|
527
|
+
}
|
|
513
528
|
/**
|
|
514
529
|
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
515
530
|
* successful delivery.
|
|
@@ -525,7 +540,62 @@ interface SmtpRejectedRecipient {
|
|
|
525
540
|
readonly response: string;
|
|
526
541
|
/** Whether retrying delivery to this recipient may succeed. */
|
|
527
542
|
readonly retryable: boolean;
|
|
543
|
+
/**
|
|
544
|
+
* The enhanced status code returned for this recipient, when valid.
|
|
545
|
+
*
|
|
546
|
+
* @since 0.6.0
|
|
547
|
+
*/
|
|
548
|
+
readonly enhancedStatusCode?: SmtpEnhancedStatusCode;
|
|
528
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Provider details for a failure returned by an SMTP server.
|
|
552
|
+
*
|
|
553
|
+
* @since 0.6.0
|
|
554
|
+
*/
|
|
555
|
+
interface SmtpResponseProviderDetails {
|
|
556
|
+
/** The SMTP command that failed. */
|
|
557
|
+
readonly command: string;
|
|
558
|
+
/** The text from the SMTP server's final reply line. */
|
|
559
|
+
readonly response: string;
|
|
560
|
+
/** Recipient-level failures collected for the transaction. */
|
|
561
|
+
readonly rejectedRecipients?: readonly SmtpRejectedRecipient[];
|
|
562
|
+
/** The enhanced status code in the server reply, when valid. */
|
|
563
|
+
readonly enhancedStatusCode?: SmtpEnhancedStatusCode;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Checks whether provider details came from an SMTP server response.
|
|
567
|
+
*
|
|
568
|
+
* @param value Provider details from an SMTP receipt error.
|
|
569
|
+
* @returns Whether the value contains SMTP response details.
|
|
570
|
+
* @since 0.6.0
|
|
571
|
+
*/
|
|
572
|
+
declare function isSmtpResponseProviderDetails(value: unknown): value is SmtpResponseProviderDetails;
|
|
573
|
+
/**
|
|
574
|
+
* Provider details for a locally rejected oversized message.
|
|
575
|
+
*
|
|
576
|
+
* @since 0.6.0
|
|
577
|
+
*/
|
|
578
|
+
interface SmtpMessageSizeProviderDetails {
|
|
579
|
+
/** The encoded message size in octets. */
|
|
580
|
+
readonly actualSize: number;
|
|
581
|
+
/** The server's advertised maximum size in octets. */
|
|
582
|
+
readonly maximumSize: string;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Provider details for a missing internationalization capability.
|
|
586
|
+
*
|
|
587
|
+
* @since 0.6.0
|
|
588
|
+
*/
|
|
589
|
+
interface SmtpUtf8ProviderDetails {
|
|
590
|
+
/** The SMTP extension required by the message. */
|
|
591
|
+
readonly missingCapability: "SMTPUTF8" | "8BITMIME";
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Provider-specific details attached to SMTP receipt errors.
|
|
595
|
+
*
|
|
596
|
+
* @since 0.6.0
|
|
597
|
+
*/
|
|
598
|
+
type SmtpProviderDetails = SmtpResponseProviderDetails | SmtpMessageSizeProviderDetails | SmtpUtf8ProviderDetails;
|
|
529
599
|
/**
|
|
530
600
|
* A receipt returned by {@link SmtpTransport}.
|
|
531
601
|
*
|
|
@@ -727,4 +797,4 @@ declare class SmtpAuthError extends Error {
|
|
|
727
797
|
*/
|
|
728
798
|
|
|
729
799
|
//#endregion
|
|
730
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth };
|
|
800
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
package/dist/index.d.ts
CHANGED
|
@@ -510,6 +510,21 @@ declare class SmtpDsnUnsupportedError extends Error {
|
|
|
510
510
|
/** @internal */
|
|
511
511
|
//#endregion
|
|
512
512
|
//#region src/smtp-receipt.d.ts
|
|
513
|
+
/**
|
|
514
|
+
* A machine-readable enhanced SMTP status code defined by RFC 3463.
|
|
515
|
+
*
|
|
516
|
+
* @since 0.6.0
|
|
517
|
+
*/
|
|
518
|
+
interface SmtpEnhancedStatusCode {
|
|
519
|
+
/** The complete enhanced status code. */
|
|
520
|
+
readonly code: string;
|
|
521
|
+
/** The delivery result class: success, transient failure, or permanent failure. */
|
|
522
|
+
readonly class: 2 | 4 | 5;
|
|
523
|
+
/** The subject identifying the probable source of the condition. */
|
|
524
|
+
readonly subject: number;
|
|
525
|
+
/** The detail identifying the precise condition. */
|
|
526
|
+
readonly detail: number;
|
|
527
|
+
}
|
|
513
528
|
/**
|
|
514
529
|
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
515
530
|
* successful delivery.
|
|
@@ -525,7 +540,62 @@ interface SmtpRejectedRecipient {
|
|
|
525
540
|
readonly response: string;
|
|
526
541
|
/** Whether retrying delivery to this recipient may succeed. */
|
|
527
542
|
readonly retryable: boolean;
|
|
543
|
+
/**
|
|
544
|
+
* The enhanced status code returned for this recipient, when valid.
|
|
545
|
+
*
|
|
546
|
+
* @since 0.6.0
|
|
547
|
+
*/
|
|
548
|
+
readonly enhancedStatusCode?: SmtpEnhancedStatusCode;
|
|
528
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Provider details for a failure returned by an SMTP server.
|
|
552
|
+
*
|
|
553
|
+
* @since 0.6.0
|
|
554
|
+
*/
|
|
555
|
+
interface SmtpResponseProviderDetails {
|
|
556
|
+
/** The SMTP command that failed. */
|
|
557
|
+
readonly command: string;
|
|
558
|
+
/** The text from the SMTP server's final reply line. */
|
|
559
|
+
readonly response: string;
|
|
560
|
+
/** Recipient-level failures collected for the transaction. */
|
|
561
|
+
readonly rejectedRecipients?: readonly SmtpRejectedRecipient[];
|
|
562
|
+
/** The enhanced status code in the server reply, when valid. */
|
|
563
|
+
readonly enhancedStatusCode?: SmtpEnhancedStatusCode;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Checks whether provider details came from an SMTP server response.
|
|
567
|
+
*
|
|
568
|
+
* @param value Provider details from an SMTP receipt error.
|
|
569
|
+
* @returns Whether the value contains SMTP response details.
|
|
570
|
+
* @since 0.6.0
|
|
571
|
+
*/
|
|
572
|
+
declare function isSmtpResponseProviderDetails(value: unknown): value is SmtpResponseProviderDetails;
|
|
573
|
+
/**
|
|
574
|
+
* Provider details for a locally rejected oversized message.
|
|
575
|
+
*
|
|
576
|
+
* @since 0.6.0
|
|
577
|
+
*/
|
|
578
|
+
interface SmtpMessageSizeProviderDetails {
|
|
579
|
+
/** The encoded message size in octets. */
|
|
580
|
+
readonly actualSize: number;
|
|
581
|
+
/** The server's advertised maximum size in octets. */
|
|
582
|
+
readonly maximumSize: string;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Provider details for a missing internationalization capability.
|
|
586
|
+
*
|
|
587
|
+
* @since 0.6.0
|
|
588
|
+
*/
|
|
589
|
+
interface SmtpUtf8ProviderDetails {
|
|
590
|
+
/** The SMTP extension required by the message. */
|
|
591
|
+
readonly missingCapability: "SMTPUTF8" | "8BITMIME";
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Provider-specific details attached to SMTP receipt errors.
|
|
595
|
+
*
|
|
596
|
+
* @since 0.6.0
|
|
597
|
+
*/
|
|
598
|
+
type SmtpProviderDetails = SmtpResponseProviderDetails | SmtpMessageSizeProviderDetails | SmtpUtf8ProviderDetails;
|
|
529
599
|
/**
|
|
530
600
|
* A receipt returned by {@link SmtpTransport}.
|
|
531
601
|
*
|
|
@@ -727,4 +797,4 @@ declare class SmtpAuthError extends Error {
|
|
|
727
797
|
*/
|
|
728
798
|
|
|
729
799
|
//#endregion
|
|
730
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth };
|
|
800
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
package/dist/index.js
CHANGED
|
@@ -449,6 +449,34 @@ var OAuth2TokenManager = class {
|
|
|
449
449
|
}
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/smtp-status-code.ts
|
|
454
|
+
/**
|
|
455
|
+
* Parses an enhanced SMTP status code from the beginning of reply text.
|
|
456
|
+
*
|
|
457
|
+
* RFC 2034 requires the enhanced class to agree with the three-digit SMTP
|
|
458
|
+
* reply class. RFC 3463 also prohibits leading zeroes and limits the subject
|
|
459
|
+
* and detail fields to three digits.
|
|
460
|
+
*
|
|
461
|
+
* @param replyCode The three-digit SMTP reply code.
|
|
462
|
+
* @param response The textual part of the SMTP reply.
|
|
463
|
+
* @returns The parsed enhanced status code, or `undefined` when the reply does
|
|
464
|
+
* not begin with a valid, consistent code.
|
|
465
|
+
* @since 0.6.0
|
|
466
|
+
*/
|
|
467
|
+
function parseEnhancedSmtpStatusCode(replyCode, response) {
|
|
468
|
+
const match = /^([245])\.((?:0|[1-9][0-9]{0,2}))\.((?:0|[1-9][0-9]{0,2})) +/.exec(response);
|
|
469
|
+
if (match == null) return void 0;
|
|
470
|
+
const statusClass = Number(match[1]);
|
|
471
|
+
if (Math.trunc(replyCode / 100) !== statusClass) return void 0;
|
|
472
|
+
return {
|
|
473
|
+
code: `${match[1]}.${match[2]}.${match[3]}`,
|
|
474
|
+
class: statusClass,
|
|
475
|
+
subject: Number(match[2]),
|
|
476
|
+
detail: Number(match[3])
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
452
480
|
//#endregion
|
|
453
481
|
//#region src/smtp-connection.ts
|
|
454
482
|
/**
|
|
@@ -512,6 +540,37 @@ var SmtpPipelineTerminatedError = class extends Error {
|
|
|
512
540
|
}
|
|
513
541
|
};
|
|
514
542
|
/**
|
|
543
|
+
* An authentication failure backed by an SMTP server reply.
|
|
544
|
+
*
|
|
545
|
+
* This keeps {@link SmtpAuthError} as the public authentication error while
|
|
546
|
+
* retaining the reply fields needed to build structured transport receipts.
|
|
547
|
+
*
|
|
548
|
+
* @since 0.6.0
|
|
549
|
+
*/
|
|
550
|
+
var SmtpAuthResponseError = class extends SmtpAuthError {
|
|
551
|
+
/** The numeric SMTP reply code returned by the server. */
|
|
552
|
+
code;
|
|
553
|
+
/** The authentication command that produced the reply. */
|
|
554
|
+
command;
|
|
555
|
+
/** The textual SMTP reply returned by the server. */
|
|
556
|
+
response;
|
|
557
|
+
/**
|
|
558
|
+
* Creates an authentication response error.
|
|
559
|
+
*
|
|
560
|
+
* @param message A human-readable description of the authentication failure.
|
|
561
|
+
* @param code The numeric SMTP reply code.
|
|
562
|
+
* @param command The authentication command that produced the reply.
|
|
563
|
+
* @param response The textual SMTP reply returned by the server.
|
|
564
|
+
*/
|
|
565
|
+
constructor(message, code, command, response) {
|
|
566
|
+
super(message);
|
|
567
|
+
this.name = "SmtpAuthResponseError";
|
|
568
|
+
this.code = code;
|
|
569
|
+
this.command = command;
|
|
570
|
+
this.response = response;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
515
574
|
* The maximum length of an SMTP command line, including the terminating CRLF,
|
|
516
575
|
* as specified by RFC 5321 §4.5.3.1.4.
|
|
517
576
|
*/
|
|
@@ -900,16 +959,16 @@ var SmtpConnection = class {
|
|
|
900
959
|
const { user, pass } = auth;
|
|
901
960
|
const credentials = btoa(`\0${user}\0${pass}`);
|
|
902
961
|
const response = await this.sendCommand(`AUTH PLAIN ${credentials}`, signal);
|
|
903
|
-
if (response.code !== 235) throw new
|
|
962
|
+
if (response.code !== 235) throw new SmtpAuthResponseError(`Authentication failed: ${response.message}`, response.code, "AUTH PLAIN", response.message);
|
|
904
963
|
}
|
|
905
964
|
async authLogin(auth, signal) {
|
|
906
965
|
const { user, pass } = auth;
|
|
907
966
|
let response = await this.sendCommand("AUTH LOGIN", signal);
|
|
908
|
-
if (response.code !== 334) throw new
|
|
967
|
+
if (response.code !== 334) throw new SmtpAuthResponseError(`AUTH LOGIN failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
909
968
|
response = await this.sendCommand(btoa(user), signal);
|
|
910
|
-
if (response.code !== 334) throw new
|
|
969
|
+
if (response.code !== 334) throw new SmtpAuthResponseError(`Username authentication failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
911
970
|
response = await this.sendCommand(btoa(pass), signal);
|
|
912
|
-
if (response.code !== 235) throw new
|
|
971
|
+
if (response.code !== 235) throw new SmtpAuthResponseError(`Password authentication failed: ${response.message}`, response.code, "AUTH LOGIN", response.message);
|
|
913
972
|
}
|
|
914
973
|
/**
|
|
915
974
|
* Resolves an OAuth 2.0 access token via the connection's token manager,
|
|
@@ -961,16 +1020,19 @@ var SmtpConnection = class {
|
|
|
961
1020
|
async finishOAuth2(response, mechanism, continuation, signal) {
|
|
962
1021
|
if (response.code === 235) return;
|
|
963
1022
|
if (response.code === 334) {
|
|
1023
|
+
let finalResponse;
|
|
964
1024
|
let finalMessage = "";
|
|
965
1025
|
try {
|
|
966
|
-
|
|
967
|
-
finalMessage = ` (${
|
|
1026
|
+
finalResponse = await this.sendCommand(continuation, signal);
|
|
1027
|
+
finalMessage = ` (${finalResponse.message})`;
|
|
968
1028
|
} catch {
|
|
969
1029
|
signal?.throwIfAborted();
|
|
970
1030
|
}
|
|
971
|
-
|
|
1031
|
+
const message = `${mechanism} authentication failed: ${decodeOAuth2Challenge(response.message)}${finalMessage}`;
|
|
1032
|
+
if (finalResponse != null) throw new SmtpAuthResponseError(message, finalResponse.code, `AUTH ${mechanism}`, finalResponse.message);
|
|
1033
|
+
throw new SmtpAuthError(message);
|
|
972
1034
|
}
|
|
973
|
-
throw new
|
|
1035
|
+
throw new SmtpAuthResponseError(`${mechanism} authentication failed: ${response.message}`, response.code, `AUTH ${mechanism}`, response.message);
|
|
974
1036
|
}
|
|
975
1037
|
async sendMessage(message, signal) {
|
|
976
1038
|
signal?.throwIfAborted();
|
|
@@ -1020,12 +1082,16 @@ var SmtpConnection = class {
|
|
|
1020
1082
|
signal?.throwIfAborted();
|
|
1021
1083
|
const rcptResponse = pipelining ? recipientResponses[index] : await this.sendCommand(recipientCommands[index], signal);
|
|
1022
1084
|
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
1023
|
-
if (rcptResponse.code !== 250 && rcptResponse.code !== 251)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1085
|
+
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) {
|
|
1086
|
+
const enhancedStatusCode = parseEnhancedSmtpStatusCode(rcptResponse.code, rcptResponse.message);
|
|
1087
|
+
rejectedRecipients.push({
|
|
1088
|
+
recipient,
|
|
1089
|
+
code: rcptResponse.code,
|
|
1090
|
+
response: rcptResponse.message,
|
|
1091
|
+
retryable: rcptResponse.code >= 400 && rcptResponse.code < 500,
|
|
1092
|
+
...enhancedStatusCode == null ? {} : { enhancedStatusCode }
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1029
1095
|
}
|
|
1030
1096
|
if (rejectedRecipients.length > 0 && rejectedRecipients.length === message.envelope.to.length) {
|
|
1031
1097
|
const rejection = rejectedRecipients.find((item) => item.retryable) ?? rejectedRecipients[0];
|
|
@@ -1994,8 +2060,9 @@ function createSmtpFailure(message, error) {
|
|
|
1994
2060
|
attempts: 1,
|
|
1995
2061
|
providerDetails: { missingCapability: error.missingCapability }
|
|
1996
2062
|
});
|
|
1997
|
-
if (error instanceof SmtpResponseError) {
|
|
1998
|
-
const
|
|
2063
|
+
if (error instanceof SmtpResponseError || error instanceof SmtpAuthResponseError) {
|
|
2064
|
+
const enhancedStatusCode = parseEnhancedSmtpStatusCode(error.code, error.response);
|
|
2065
|
+
const classification = classifySmtpReply(error.code, enhancedStatusCode);
|
|
1999
2066
|
return createFailedReceipt(message, {
|
|
2000
2067
|
provider: "smtp",
|
|
2001
2068
|
code: `smtp.${error.code}`,
|
|
@@ -2005,7 +2072,8 @@ function createSmtpFailure(message, error) {
|
|
|
2005
2072
|
providerDetails: {
|
|
2006
2073
|
command: error.command,
|
|
2007
2074
|
response: error.response,
|
|
2008
|
-
rejectedRecipients: error.rejectedRecipients
|
|
2075
|
+
rejectedRecipients: error instanceof SmtpResponseError ? error.rejectedRecipients : void 0,
|
|
2076
|
+
...enhancedStatusCode == null ? {} : { enhancedStatusCode }
|
|
2009
2077
|
}
|
|
2010
2078
|
});
|
|
2011
2079
|
}
|
|
@@ -2017,14 +2085,23 @@ function createSmtpFailure(message, error) {
|
|
|
2017
2085
|
function isReusableLocalFailure(error) {
|
|
2018
2086
|
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2019
2087
|
}
|
|
2020
|
-
function classifySmtpReply(code) {
|
|
2088
|
+
function classifySmtpReply(code, enhancedStatusCode) {
|
|
2089
|
+
const retryable = enhancedStatusCode == null ? code >= 400 && code < 500 : enhancedStatusCode.class === 4;
|
|
2090
|
+
if (enhancedStatusCode?.subject === 1 || enhancedStatusCode?.subject === 6) return {
|
|
2091
|
+
category: "validation",
|
|
2092
|
+
retryable
|
|
2093
|
+
};
|
|
2094
|
+
if (enhancedStatusCode?.subject === 4) return {
|
|
2095
|
+
category: "network",
|
|
2096
|
+
retryable
|
|
2097
|
+
};
|
|
2021
2098
|
if (code >= 400 && code < 500) return {
|
|
2022
2099
|
category: "service-unavailable",
|
|
2023
|
-
retryable
|
|
2100
|
+
retryable
|
|
2024
2101
|
};
|
|
2025
2102
|
if (code >= 500 && code < 600) return {
|
|
2026
2103
|
category: "rejected",
|
|
2027
|
-
retryable
|
|
2104
|
+
retryable
|
|
2028
2105
|
};
|
|
2029
2106
|
return {
|
|
2030
2107
|
category: "unknown",
|
|
@@ -2033,4 +2110,17 @@ function classifySmtpReply(code) {
|
|
|
2033
2110
|
}
|
|
2034
2111
|
|
|
2035
2112
|
//#endregion
|
|
2036
|
-
|
|
2113
|
+
//#region src/smtp-receipt.ts
|
|
2114
|
+
/**
|
|
2115
|
+
* Checks whether provider details came from an SMTP server response.
|
|
2116
|
+
*
|
|
2117
|
+
* @param value Provider details from an SMTP receipt error.
|
|
2118
|
+
* @returns Whether the value contains SMTP response details.
|
|
2119
|
+
* @since 0.6.0
|
|
2120
|
+
*/
|
|
2121
|
+
function isSmtpResponseProviderDetails(value) {
|
|
2122
|
+
return value != null && typeof value === "object" && "command" in value && typeof value.command === "string" && "response" in value && typeof value.response === "string";
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
//#endregion
|
|
2126
|
+
export { SmtpAuthError, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpTransport, isSmtpResponseProviderDetails };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/smtp",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.301",
|
|
4
4
|
"description": "SMTP transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.6.0-dev.
|
|
56
|
+
"@upyo/core": "0.6.0-dev.301+877ca1c9"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|