@upyo/smtp 0.6.0-dev.287 → 0.6.0-dev.289
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 +15 -0
- package/dist/index.cjs +70 -5
- package/dist/index.js +70 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Features
|
|
|
27
27
|
- File attachments (regular and inline)
|
|
28
28
|
- Multiple recipients (To, CC, BCC)
|
|
29
29
|
- SMTP PIPELINING for faster multi-recipient delivery
|
|
30
|
+
- SMTP SIZE declaration and advertised-limit checks
|
|
30
31
|
- Custom headers
|
|
31
32
|
- Priority levels
|
|
32
33
|
- Comprehensive testing utilities
|
|
@@ -167,6 +168,20 @@ token across pooled connections. See the
|
|
|
167
168
|
[oauth-guide]: https://upyo.org/transports/smtp#oauth-2-0-authentication
|
|
168
169
|
|
|
169
170
|
|
|
171
|
+
Message size limits
|
|
172
|
+
-------------------
|
|
173
|
+
|
|
174
|
+
When the server advertises the `SIZE` extension, the transport declares the
|
|
175
|
+
encoded message size on `MAIL FROM`. If the server also advertises a fixed
|
|
176
|
+
maximum, an oversized message produces a failed receipt before Upyo sends the
|
|
177
|
+
envelope or uploads the message. Bare `SIZE` and `SIZE 0` advertisements do not
|
|
178
|
+
set a fixed maximum. This behavior is automatic and needs no configuration.
|
|
179
|
+
|
|
180
|
+
See [RFC 1870] for the SMTP Message Size Declaration extension.
|
|
181
|
+
|
|
182
|
+
[RFC 1870]: https://www.rfc-editor.org/rfc/rfc1870
|
|
183
|
+
|
|
184
|
+
|
|
170
185
|
DKIM signing
|
|
171
186
|
------------
|
|
172
187
|
|
package/dist/index.cjs
CHANGED
|
@@ -22,9 +22,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
24
|
const __upyo_core = __toESM(require("@upyo/core"));
|
|
25
|
+
const node_buffer = __toESM(require("node:buffer"));
|
|
25
26
|
const node_net = __toESM(require("node:net"));
|
|
26
27
|
const node_tls = __toESM(require("node:tls"));
|
|
27
|
-
const node_buffer = __toESM(require("node:buffer"));
|
|
28
28
|
|
|
29
29
|
//#region src/config.ts
|
|
30
30
|
/**
|
|
@@ -361,6 +361,33 @@ var OAuth2TokenManager = class {
|
|
|
361
361
|
|
|
362
362
|
//#endregion
|
|
363
363
|
//#region src/smtp-connection.ts
|
|
364
|
+
/**
|
|
365
|
+
* Error thrown when a message exceeds the fixed limit advertised through the
|
|
366
|
+
* SMTP SIZE extension.
|
|
367
|
+
*
|
|
368
|
+
* The check happens before `MAIL FROM`, so the SMTP connection remains usable
|
|
369
|
+
* for another message.
|
|
370
|
+
*
|
|
371
|
+
* @since 0.6.0
|
|
372
|
+
*/
|
|
373
|
+
var SmtpMessageSizeError = class extends RangeError {
|
|
374
|
+
/** The encoded message size in octets. */
|
|
375
|
+
actualSize;
|
|
376
|
+
/** The fixed maximum advertised by the SMTP server. */
|
|
377
|
+
maximumSize;
|
|
378
|
+
/**
|
|
379
|
+
* Creates an SMTP message-size error.
|
|
380
|
+
*
|
|
381
|
+
* @param actualSize The encoded message size in octets.
|
|
382
|
+
* @param maximumSize The fixed maximum advertised by the SMTP server.
|
|
383
|
+
*/
|
|
384
|
+
constructor(actualSize, maximumSize) {
|
|
385
|
+
super(`Message size ${actualSize} octets exceeds the server's maximum of ${maximumSize} octets.`);
|
|
386
|
+
this.name = "SmtpMessageSizeError";
|
|
387
|
+
this.actualSize = actualSize;
|
|
388
|
+
this.maximumSize = maximumSize;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
364
391
|
var SmtpPipelineTerminatedError = class extends Error {
|
|
365
392
|
responseIndex;
|
|
366
393
|
response;
|
|
@@ -379,6 +406,24 @@ const MAX_COMMAND_LINE_LENGTH = 512;
|
|
|
379
406
|
/** The length of the CRLF terminator appended to every command. */
|
|
380
407
|
const CRLF_LENGTH = 2;
|
|
381
408
|
/**
|
|
409
|
+
* Finds the RFC 1870 SIZE extension and its optional fixed maximum.
|
|
410
|
+
*
|
|
411
|
+
* A zero maximum and an omitted maximum both mean that no fixed limit is in
|
|
412
|
+
* force. A malformed parameter does not prevent use of the extension, but it
|
|
413
|
+
* cannot be used as a local limit.
|
|
414
|
+
*
|
|
415
|
+
* @param capabilities The extension lines returned by EHLO.
|
|
416
|
+
* @returns The SIZE capability, or `null` when it was not advertised.
|
|
417
|
+
*/
|
|
418
|
+
function parseSizeCapability(capabilities) {
|
|
419
|
+
const capability = capabilities.find((value) => /^SIZE(?:[ \t]|$)/i.test(value));
|
|
420
|
+
if (capability == null) return null;
|
|
421
|
+
const match = /^SIZE(?:[ \t]+([0-9]+))?[ \t]*$/i.exec(capability);
|
|
422
|
+
if (match?.[1] == null) return { maximum: null };
|
|
423
|
+
const maximum = BigInt(match[1]);
|
|
424
|
+
return { maximum: maximum === 0n ? null : maximum };
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
382
427
|
* How long, in milliseconds, to wait for the graceful `QUIT` to flush during
|
|
383
428
|
* teardown before giving up, so an unresponsive server cannot block shutdown
|
|
384
429
|
* for the full socket timeout.
|
|
@@ -815,7 +860,15 @@ var SmtpConnection = class {
|
|
|
815
860
|
throw new SmtpAuthError(`${mechanism} authentication failed: ${response.message}`);
|
|
816
861
|
}
|
|
817
862
|
async sendMessage(message, signal) {
|
|
818
|
-
|
|
863
|
+
signal?.throwIfAborted();
|
|
864
|
+
const sizeCapability = parseSizeCapability(this.capabilities);
|
|
865
|
+
let sizeParameter = "";
|
|
866
|
+
if (sizeCapability != null) {
|
|
867
|
+
const messageSize = node_buffer.Buffer.byteLength(message.raw, "utf8") + CRLF_LENGTH;
|
|
868
|
+
if (sizeCapability.maximum != null && BigInt(messageSize) > sizeCapability.maximum) throw new SmtpMessageSizeError(messageSize, sizeCapability.maximum);
|
|
869
|
+
sizeParameter = ` SIZE=${messageSize}`;
|
|
870
|
+
}
|
|
871
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}`;
|
|
819
872
|
const recipientCommands = message.envelope.to.map((recipient) => `RCPT TO:<${recipient}>`);
|
|
820
873
|
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
821
874
|
let mailResponse;
|
|
@@ -1581,7 +1634,8 @@ var SmtpTransport = class {
|
|
|
1581
1634
|
rejectedRecipients: result.rejectedRecipients
|
|
1582
1635
|
};
|
|
1583
1636
|
} catch (error) {
|
|
1584
|
-
if (connection != null) await this.
|
|
1637
|
+
if (connection != null) if (error instanceof SmtpMessageSizeError) await this.returnConnection(connection);
|
|
1638
|
+
else await this.discardConnection(connection);
|
|
1585
1639
|
options?.signal?.throwIfAborted();
|
|
1586
1640
|
return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1587
1641
|
}
|
|
@@ -1651,7 +1705,7 @@ var SmtpTransport = class {
|
|
|
1651
1705
|
};
|
|
1652
1706
|
} catch (error) {
|
|
1653
1707
|
options?.signal?.throwIfAborted();
|
|
1654
|
-
connectionValid = false;
|
|
1708
|
+
if (!(error instanceof SmtpMessageSizeError)) connectionValid = false;
|
|
1655
1709
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1656
1710
|
}
|
|
1657
1711
|
}
|
|
@@ -1673,7 +1727,7 @@ var SmtpTransport = class {
|
|
|
1673
1727
|
};
|
|
1674
1728
|
} catch (error) {
|
|
1675
1729
|
options?.signal?.throwIfAborted();
|
|
1676
|
-
connectionValid = false;
|
|
1730
|
+
if (!(error instanceof SmtpMessageSizeError)) connectionValid = false;
|
|
1677
1731
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1678
1732
|
}
|
|
1679
1733
|
}
|
|
@@ -1768,6 +1822,17 @@ var SmtpTransport = class {
|
|
|
1768
1822
|
}
|
|
1769
1823
|
};
|
|
1770
1824
|
function createSmtpFailure(message, error) {
|
|
1825
|
+
if (error instanceof SmtpMessageSizeError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
1826
|
+
provider: "smtp",
|
|
1827
|
+
code: "smtp.message-size-exceeded",
|
|
1828
|
+
category: "rejected",
|
|
1829
|
+
retryable: false,
|
|
1830
|
+
attempts: 1,
|
|
1831
|
+
providerDetails: {
|
|
1832
|
+
actualSize: error.actualSize,
|
|
1833
|
+
maximumSize: error.maximumSize.toString()
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1771
1836
|
if (error instanceof SmtpResponseError) {
|
|
1772
1837
|
const classification = classifySmtpReply(error.code);
|
|
1773
1838
|
return (0, __upyo_core.createFailedReceipt)(message, {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createFailedReceipt } from "@upyo/core";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
2
3
|
import { Socket } from "node:net";
|
|
3
4
|
import { TLSSocket, connect } from "node:tls";
|
|
4
|
-
import { Buffer } from "node:buffer";
|
|
5
5
|
|
|
6
6
|
//#region src/config.ts
|
|
7
7
|
/**
|
|
@@ -338,6 +338,33 @@ var OAuth2TokenManager = class {
|
|
|
338
338
|
|
|
339
339
|
//#endregion
|
|
340
340
|
//#region src/smtp-connection.ts
|
|
341
|
+
/**
|
|
342
|
+
* Error thrown when a message exceeds the fixed limit advertised through the
|
|
343
|
+
* SMTP SIZE extension.
|
|
344
|
+
*
|
|
345
|
+
* The check happens before `MAIL FROM`, so the SMTP connection remains usable
|
|
346
|
+
* for another message.
|
|
347
|
+
*
|
|
348
|
+
* @since 0.6.0
|
|
349
|
+
*/
|
|
350
|
+
var SmtpMessageSizeError = class extends RangeError {
|
|
351
|
+
/** The encoded message size in octets. */
|
|
352
|
+
actualSize;
|
|
353
|
+
/** The fixed maximum advertised by the SMTP server. */
|
|
354
|
+
maximumSize;
|
|
355
|
+
/**
|
|
356
|
+
* Creates an SMTP message-size error.
|
|
357
|
+
*
|
|
358
|
+
* @param actualSize The encoded message size in octets.
|
|
359
|
+
* @param maximumSize The fixed maximum advertised by the SMTP server.
|
|
360
|
+
*/
|
|
361
|
+
constructor(actualSize, maximumSize) {
|
|
362
|
+
super(`Message size ${actualSize} octets exceeds the server's maximum of ${maximumSize} octets.`);
|
|
363
|
+
this.name = "SmtpMessageSizeError";
|
|
364
|
+
this.actualSize = actualSize;
|
|
365
|
+
this.maximumSize = maximumSize;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
341
368
|
var SmtpPipelineTerminatedError = class extends Error {
|
|
342
369
|
responseIndex;
|
|
343
370
|
response;
|
|
@@ -356,6 +383,24 @@ const MAX_COMMAND_LINE_LENGTH = 512;
|
|
|
356
383
|
/** The length of the CRLF terminator appended to every command. */
|
|
357
384
|
const CRLF_LENGTH = 2;
|
|
358
385
|
/**
|
|
386
|
+
* Finds the RFC 1870 SIZE extension and its optional fixed maximum.
|
|
387
|
+
*
|
|
388
|
+
* A zero maximum and an omitted maximum both mean that no fixed limit is in
|
|
389
|
+
* force. A malformed parameter does not prevent use of the extension, but it
|
|
390
|
+
* cannot be used as a local limit.
|
|
391
|
+
*
|
|
392
|
+
* @param capabilities The extension lines returned by EHLO.
|
|
393
|
+
* @returns The SIZE capability, or `null` when it was not advertised.
|
|
394
|
+
*/
|
|
395
|
+
function parseSizeCapability(capabilities) {
|
|
396
|
+
const capability = capabilities.find((value) => /^SIZE(?:[ \t]|$)/i.test(value));
|
|
397
|
+
if (capability == null) return null;
|
|
398
|
+
const match = /^SIZE(?:[ \t]+([0-9]+))?[ \t]*$/i.exec(capability);
|
|
399
|
+
if (match?.[1] == null) return { maximum: null };
|
|
400
|
+
const maximum = BigInt(match[1]);
|
|
401
|
+
return { maximum: maximum === 0n ? null : maximum };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
359
404
|
* How long, in milliseconds, to wait for the graceful `QUIT` to flush during
|
|
360
405
|
* teardown before giving up, so an unresponsive server cannot block shutdown
|
|
361
406
|
* for the full socket timeout.
|
|
@@ -792,7 +837,15 @@ var SmtpConnection = class {
|
|
|
792
837
|
throw new SmtpAuthError(`${mechanism} authentication failed: ${response.message}`);
|
|
793
838
|
}
|
|
794
839
|
async sendMessage(message, signal) {
|
|
795
|
-
|
|
840
|
+
signal?.throwIfAborted();
|
|
841
|
+
const sizeCapability = parseSizeCapability(this.capabilities);
|
|
842
|
+
let sizeParameter = "";
|
|
843
|
+
if (sizeCapability != null) {
|
|
844
|
+
const messageSize = Buffer.byteLength(message.raw, "utf8") + CRLF_LENGTH;
|
|
845
|
+
if (sizeCapability.maximum != null && BigInt(messageSize) > sizeCapability.maximum) throw new SmtpMessageSizeError(messageSize, sizeCapability.maximum);
|
|
846
|
+
sizeParameter = ` SIZE=${messageSize}`;
|
|
847
|
+
}
|
|
848
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}`;
|
|
796
849
|
const recipientCommands = message.envelope.to.map((recipient) => `RCPT TO:<${recipient}>`);
|
|
797
850
|
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
798
851
|
let mailResponse;
|
|
@@ -1558,7 +1611,8 @@ var SmtpTransport = class {
|
|
|
1558
1611
|
rejectedRecipients: result.rejectedRecipients
|
|
1559
1612
|
};
|
|
1560
1613
|
} catch (error) {
|
|
1561
|
-
if (connection != null) await this.
|
|
1614
|
+
if (connection != null) if (error instanceof SmtpMessageSizeError) await this.returnConnection(connection);
|
|
1615
|
+
else await this.discardConnection(connection);
|
|
1562
1616
|
options?.signal?.throwIfAborted();
|
|
1563
1617
|
return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1564
1618
|
}
|
|
@@ -1628,7 +1682,7 @@ var SmtpTransport = class {
|
|
|
1628
1682
|
};
|
|
1629
1683
|
} catch (error) {
|
|
1630
1684
|
options?.signal?.throwIfAborted();
|
|
1631
|
-
connectionValid = false;
|
|
1685
|
+
if (!(error instanceof SmtpMessageSizeError)) connectionValid = false;
|
|
1632
1686
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1633
1687
|
}
|
|
1634
1688
|
}
|
|
@@ -1650,7 +1704,7 @@ var SmtpTransport = class {
|
|
|
1650
1704
|
};
|
|
1651
1705
|
} catch (error) {
|
|
1652
1706
|
options?.signal?.throwIfAborted();
|
|
1653
|
-
connectionValid = false;
|
|
1707
|
+
if (!(error instanceof SmtpMessageSizeError)) connectionValid = false;
|
|
1654
1708
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1655
1709
|
}
|
|
1656
1710
|
}
|
|
@@ -1745,6 +1799,17 @@ var SmtpTransport = class {
|
|
|
1745
1799
|
}
|
|
1746
1800
|
};
|
|
1747
1801
|
function createSmtpFailure(message, error) {
|
|
1802
|
+
if (error instanceof SmtpMessageSizeError) return createFailedReceipt(message, {
|
|
1803
|
+
provider: "smtp",
|
|
1804
|
+
code: "smtp.message-size-exceeded",
|
|
1805
|
+
category: "rejected",
|
|
1806
|
+
retryable: false,
|
|
1807
|
+
attempts: 1,
|
|
1808
|
+
providerDetails: {
|
|
1809
|
+
actualSize: error.actualSize,
|
|
1810
|
+
maximumSize: error.maximumSize.toString()
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1748
1813
|
if (error instanceof SmtpResponseError) {
|
|
1749
1814
|
const classification = classifySmtpReply(error.code);
|
|
1750
1815
|
return createFailedReceipt(message, {
|
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.289",
|
|
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.289+b1231421"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|