Compare commits

...

82 Commits
v3.4.0 ... main

Author SHA1 Message Date
eleith fc175932f6
Merge pull request #342 from eleith/eleith-timeout-type
prepare more fine grained type for node 20
2023-09-05 20:45:34 -07:00
eleith e11fac696b prepare more fine grained type for node 20
fixes #341
2023-09-05 20:42:19 -07:00
eleith 3a4ba01e01 release 4.0.2 2023-05-12 15:20:28 -07:00
eleith 426d270068 update lint actions to node16 2023-05-12 15:11:57 -07:00
eleith 03694001e5 update to node16 actions
https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2023-05-12 15:06:59 -07:00
eleith ed54a25008 disable macOS tests
our macOS tests fails more than half the time while linux and windows
pass. the errors are always due to timeout issues.

while we can improve the performance of our tests (particularly
test/messages.ts) sometimes the timeouts happen when testing against the
local SMTPServer

as of now, we can't get insight into whether our tests or passing or
failing as the majority of test fails when macOS is included.

github is away of the issue: https://github.com/actions/runner-images/issues/3885
2023-05-12 14:57:04 -07:00
eleith 70b89d9361 unskip a test 2023-05-12 14:49:45 -07:00
eleith a6063e44a6 redact password in authentication.failed error msg
a few other improvements are also included

1. on invalid smtp client tests, use localhost instead of 'bah.baz' as
   it was causing significant network slowness on linux

2. upgrade ts-node to deal with some changes in node 18.5+
2023-05-12 14:46:51 -07:00
eleith 8b3c0b16f8 version bump 2022-08-12 15:36:07 -07:00
Zack Schuster 7d772326d9 chore: upgrade deps 2022-04-29 11:59:37 -07:00
Zack Schuster a395f862ec ci: upgrade lint workflow from node 17 to 18 2022-04-29 11:58:48 -07:00
Zack Schuster 4f45799a3e smtp/connection: fix custom log fn assignment 2022-04-26 19:14:14 -07:00
Zack Schuster 9a5ce38186 chore: upgrade deps 2022-04-26 18:30:11 -07:00
Zack Schuster a7b468908f ci: swap node 17 for 18 in matrix 2022-04-21 14:37:42 -07:00
Zack Schuster 793fa7466c chore: upgrade deps 2022-04-21 14:37:05 -07:00
eleith bdf156971e version 4.0.0 release 2022-04-20 15:22:37 -07:00
eleith 8c67cf0160
Merge pull request #312 from eleith/v4.0
release: 4.0
2022-04-20 15:13:19 -07:00
eleith 6dc55e013e version v3.8.1 2022-04-15 13:35:16 -07:00
Zack Schuster c0f7a171ea chore: improve node 12 compat 2022-04-14 12:22:31 -07:00
Zack Schuster 2fc0e8c493 test: fix node 12 2022-04-14 12:00:55 -07:00
Zack Schuster 851b345d33 test: always run build first 2022-04-14 11:31:40 -07:00
Zack Schuster f242b96dae test/message: attempt to improve perf 2022-04-14 11:31:40 -07:00
Zack Schuster 79a81538aa chore: use extensions in module specifiers 2022-04-14 11:31:40 -07:00
Zack Schuster 652684486c smtp/client: re-strengthen send & sendAsync types 2022-04-14 11:31:40 -07:00
Zack Schuster 5ee7d3b3b8 build: only export esm version 2022-04-14 11:31:40 -07:00
Zack Schuster f0cd1ce544 chore: upgrade deps & drop node 10 support 2022-04-14 11:31:40 -07:00
eleith b91cf6c97f
Merge pull request #310 from eleith/v3.8.1
release: v3.9.0
2022-04-13 15:48:45 -07:00
Zack Schuster 24c313669c release: update changelog 2022-04-13 10:00:11 -07:00
Zack Schuster 1d905b0d66 build: update bundles 2022-04-13 09:53:53 -07:00
Zack Schuster f064ff1302 tsc: compat with v3.8.3 2022-04-13 09:53:53 -07:00
Jeremy Möglich 9d5b5376db seperated type and normal imports 2022-04-04 10:32:00 -07:00
Zack Schuster 54e03335bf build: clean up config 2022-03-17 14:04:16 -07:00
Zack Schuster 6ae32a4c8e chore: update changelog 2022-03-17 14:04:16 -07:00
Zack Schuster f9b84cf0fd build: update bundles 2022-03-17 13:56:22 -07:00
Zack Schuster c1d0aee0b1 chore: tweak manifest 2022-03-17 13:56:17 -07:00
eleith 607de0f6a6 version 3.8.0 npm release 2022-03-17 11:00:10 -07:00
eleith a965d9bc42 version 3.7.0 npm release 2022-03-17 10:59:17 -07:00
Zack Schuster e38e1b426f lint: enable explicit-module-boundary-types 2022-03-16 12:38:28 -07:00
Zack Schuster 8496793ed6 chore: upgrade deps 2022-03-16 12:33:00 -07:00
Zack Schuster 495d8fc838 smtp/client: fix send types to match README 2022-03-16 12:31:00 -07:00
Zack Schuster d184a82bfe test: use less common port numbers 2022-03-03 15:44:35 -08:00
Zack Schuster 6d48c82aaf chore: upgrade deps 2022-03-03 15:32:20 -08:00
Zack Schuster 9c0735ea89 release: update changelog 2021-11-19 16:58:04 -08:00
Zack Schuster 87b86299a0 build: update bundles 2021-11-19 15:58:03 -08:00
Zack Schuster 9f7ddf0c6b release: update changelog 2021-11-19 15:57:31 -08:00
Zack Schuster 89c0d574b0 smtp/date: don't compile regex 2021-11-19 15:52:06 -08:00
Zack Schuster bceb38d8aa chore: upgrade deps 2021-11-19 15:51:32 -08:00
Zack Schuster e801eb54c9 chore: downgrade eslint 2021-10-27 08:10:14 -07:00
Zack Schuster 1fe1557ca9 build: add node 17 to workflows 2021-10-27 08:07:36 -07:00
Zack Schuster 98346475e2 chore: upgrade deps 2021-10-27 08:07:18 -07:00
Zack Schuster 0480014ed4 release: update changelog date 2021-10-27 07:58:16 -07:00
eleith 99cf10fea8 version 3.6.0 npm release 2021-09-03 16:53:14 -07:00
eleith 6ee37bfeb7
Merge pull request #299 from eleith/v3.6.0
release: update for 3.6.0
2021-09-03 16:50:15 -07:00
Zack Schuster ffd17bca34 build: update bundles 2021-09-03 15:25:17 -07:00
Zack Schuster d1424fb49f release: update changelog 2021-09-03 15:24:31 -07:00
Zack Schuster 52b82711f1 chore: add example to readme 2021-09-03 15:23:58 -07:00
Zack Schuster 0d497e9072 test: prepare for ts4.4 2021-09-03 15:23:58 -07:00
Zack Schuster c31412f0ea chore: upgrade deps
typescript update blocked due to https://github.com/microsoft/TypeScript/issues/45633
2021-09-03 15:23:58 -07:00
Zack Schuster 82c5bffac4 chore: add example to readme 2021-09-03 15:22:47 -07:00
Zack Schuster 4374122320 test: prepare for ts4.4 2021-09-03 15:22:34 -07:00
Zack Schuster a2b240d177 chore: upgrade deps
typescript update blocked due to https://github.com/microsoft/TypeScript/issues/45633
2021-09-03 15:22:27 -07:00
Zack Schuster 81cc94b929 message: add `checkValidity` method & deprecate `valid` 2021-09-03 14:54:53 -07:00
Zack Schuster 6f435a402e message: add `checkValidity` method & deprecate `valid` 2021-08-28 19:28:41 -07:00
Carson Full 0516e3825c feat: Message readAsync 2021-08-28 19:27:33 -07:00
Zack Schuster 63401a0868 build: fix workflows for node 10 2021-08-28 12:10:14 -07:00
Zack Schuster d7b9236ed0 chore: upgrade deps 2021-08-28 11:54:08 -07:00
Carson Full 502196e350 build: disable interop in rollup & tsconfig 2021-08-28 08:43:27 -07:00
Carson Full 177a03595b fix: use named imports for file functions
This removes the need for esModuleInterop
2021-08-28 08:43:27 -07:00
Zack Schuster aa35ea6b86 test: fix compile errors 2021-07-07 06:21:10 -07:00
Zack Schuster 416b3e59c6 release: update changelog date 2021-06-28 17:33:26 -07:00
eleith 30aef9ab8c bump 3.5.0 2021-06-28 11:26:50 -07:00
eleith 78d83bfb88
Merge pull request #291 from eleith/v3.5.0
release: update for 3.5.0
2021-06-28 11:14:05 -07:00
Zack Schuster e2adced6e4 build: update bundles 2021-06-27 12:53:59 -07:00
Zack Schuster 306c5e9acc release: update changelog 2021-06-27 12:53:59 -07:00
Zack Schuster 2600a79d93 chore: upgrade deps 2021-06-27 12:53:31 -07:00
Zack Schuster aa0add8b9d test/client: remove extraneous event emitter usage 2021-06-26 12:23:13 -07:00
Herr Ritschwumm 512dbac584 appease the compiler 2021-06-26 11:58:45 -07:00
Herr Ritschwumm 1d488a49ab enable noPropertyAccessFromIndexSignature 2021-06-26 11:58:45 -07:00
Zack Schuster a6973c31ec chore: update workflow config 2021-05-20 08:21:45 -07:00
Zack Schuster f46b85ce3a chore: fix engines meta 2021-05-20 08:19:29 -07:00
Zack Schuster 723b68ed19 chore: upgrade deps 2021-05-20 08:18:15 -07:00
Zack Schuster 99bdf2fb14 chore: add async/await example to readme 2020-12-01 15:20:43 -08:00
31 changed files with 1765 additions and 4304 deletions

View File

@ -9,7 +9,6 @@
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": [
"error",
{

View File

@ -7,15 +7,15 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [^10, ^12, ^14, ^15]
node: [^12, ^14, ^16, ^18]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: node
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

View File

@ -7,15 +7,15 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [^10, ^12, ^14, ^15]
os: [ubuntu-latest, windows-latest, macos-latest]
node: [^12, ^14, ^16, ^18]
os: [ubuntu-latest, windows-latest]
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: node
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
@ -24,6 +24,3 @@ jobs:
- name: test
run: yarn test
- name: test-cjs
run: yarn test-cjs

View File

@ -70,6 +70,7 @@ scripts
spec
test
test-*
!test-*.d.ts
tests
tsserverlibrary.*
typescriptservices.*

View File

@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [4.0.2] - 2023-05-12
### Fixed
- redact passwords in error messages [#339](https://github.com/eleith/emailjs/issues/339)
## [4.0.0] - 2022-04-20
### Added
- support `isolatedModules` and `preserveValueImports` compilation scenarios [#305](https://github.com/eleith/emailjs/pull/305)
### Fixed
- support `typescript@3.8.3` [#307](https://github.com/eleith/emailjs/issues/307)
- the types change in `v3.8.0` for `Client#send` & `Client#sendAsync` unintentionally raised the minimum `typescript` requirement. fixing this involved weakening the types for those functions, which may require modifying your code. this change will be reverted for `v4.0.0`.
## [3.8.0] - 2022-03-17
### Added
- support `typescript@4.6`
- type allow `Client#send` & `Client#sendAsync` to accept message headers instead of a `Message`
- no behavior change: this was previously allowed, but the types didn't acknowledge it
## [3.7.0] - 2021-11-19
### Added
- support `typescript@4.5`
## [3.6.0] - 2021-09-03
### Added
- support `tsc` compilation without `--esModuleInterop` or `--allowSyntheticDefaultImports` [#296](https://github.com/eleith/emailjs/pull/296)
- `Message#readAsync` API [#297](https://github.com/eleith/emailjs/pull/297)
- `Message#checkValidity` API [#298](https://github.com/eleith/emailjs/pull/298)
### Deprecated
- `Message#valid` API [#298](https://github.com/eleith/emailjs/pull/298)
## [3.5.0] - 2021-06-28
### Added
- support `tsc --noPropertyAccessFromIndexSignature` [#290](https://github.com/eleith/emailjs/pull/290)
### Fixed
- use `engines` field in `package.json` to signal node version support
## [3.4.0] - 2020-12-01
### Added
- `SMTPClient#sendAsync` API [#267](https://github.com/eleith/emailjs/issues/267)

View File

@ -21,7 +21,6 @@ send emails, html and attachments (files, streams and strings) from node.js to a
- auth access to an SMTP Server
- if your service (ex: gmail) uses two-step authentication, use an application specific password
- if using TypeScript, enable [`esModuleInterop`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#support-for-import-d-from-cjs-from-commonjs-modules-with---esmoduleinterop) or [`allowSyntheticDefaultImports`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-8.html#support-for-default-import-interop-with-systemjs)
## EXAMPLE USAGE - text only emails
@ -50,6 +49,33 @@ client.send(
);
```
## EXAMPLE USAGE - using async/await
```js
// assuming top-level await for brevity
import { SMTPClient } from 'emailjs';
const client = new SMTPClient({
user: 'user',
password: 'password',
host: 'smtp.your-email.com',
ssl: true,
});
try {
const message = await client.sendAsync({
text: 'i hope this works',
from: 'you <username@your-email.com>',
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
cc: 'else <else@your-email.com>',
subject: 'testing emailjs',
});
console.log(message);
} catch (err) {
console.error(err);
}
```
## EXAMPLE USAGE - html emails and attachments
```js
@ -236,6 +262,21 @@ const options = {
};
```
## Message#checkValidity()
Synchronously validate that a Message is properly formed.
```js
const message = new Message(options);
const { isValid, validationError } = message.checkValidity();
if (isValid) {
// ...
} else {
// first error encountered
console.error(validationError);
}
```
## new SMTPConnection(options={})
```js

View File

@ -1,8 +1,12 @@
export default {
files: ['test/*.ts'],
extensions: ['ts'],
require: ['./email.test.ts'],
extensions: {
ts: 'module',
},
environmentVariables: {
NODE_TLS_REJECT_UNAUTHORIZED: '0',
},
files: ['test/*.ts'],
nodeArguments: ['--loader=ts-node/esm'],
// makes tests far slower
workerThreads: false,
};

View File

@ -1,4 +1,4 @@
import fs from 'fs';
import { existsSync, open, read, closeSync, close } from 'fs';
import { hostname } from 'os';
import { Stream } from 'stream';
import { TextEncoder, TextDecoder } from 'util';
@ -257,7 +257,7 @@ function getRFC2822DateUTC(date = new Date()) {
* @see https://tools.ietf.org/html/rfc2822#section-3.3
* @see https://github.com/moment/moment/blob/a831fc7e2694281ce31e4f090bbcf90a690f0277/src/lib/create/from-string.js#L101
*/
const rfc2822re = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/.compile();
const rfc2822re = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;
/**
* @param {string} [date] a string to check for conformance to the [rfc2822](https://tools.ietf.org/html/rfc2822#section-3.3) standard
* @returns {boolean} the result of the conformance check
@ -276,7 +276,7 @@ const RANGES = [
[0x0a],
[0x0d],
[0x20, 0x3c],
[0x3e, 0x7e],
[0x3e, 0x7e], // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
];
const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
const MAX_CHUNK_LENGTH = 16383; // must be multiple of 3
@ -453,7 +453,7 @@ function mimeWordEncode(data, mimeWordEncoding = 'Q', encoding = 'utf-8') {
.trim();
}
const CRLF = '\r\n';
const CRLF$1 = '\r\n';
/**
* MIME standard wants 76 char chunks when sending out.
*/
@ -565,30 +565,32 @@ class Message {
}
/**
* @public
* @param {function(isValid: boolean, invalidReason: string): void} callback .
* @returns {void}
* @returns {{ isValid: boolean, validationError: (string | undefined) }} an object specifying whether this message is validly formatted, and the first validation error if it is not.
*/
valid(callback) {
checkValidity() {
if (typeof this.header.from !== 'string' &&
Array.isArray(this.header.from) === false) {
callback(false, 'Message must have a `from` header');
return {
isValid: false,
validationError: 'Message must have a `from` header',
};
}
else if (typeof this.header.to !== 'string' &&
if (typeof this.header.to !== 'string' &&
Array.isArray(this.header.to) === false &&
typeof this.header.cc !== 'string' &&
Array.isArray(this.header.cc) === false &&
typeof this.header.bcc !== 'string' &&
Array.isArray(this.header.bcc) === false) {
callback(false, 'Message must have at least one `to`, `cc`, or `bcc` header');
return {
isValid: false,
validationError: 'Message must have at least one `to`, `cc`, or `bcc` header',
};
}
else if (this.attachments.length === 0) {
callback(true, undefined);
}
else {
if (this.attachments.length > 0) {
const failed = [];
this.attachments.forEach((attachment) => {
if (attachment.path) {
if (fs.existsSync(attachment.path) == false) {
if (existsSync(attachment.path) === false) {
failed.push(`${attachment.path} does not exist`);
}
}
@ -601,8 +603,22 @@ class Message {
failed.push('attachment has no data associated with it');
}
});
callback(failed.length === 0, failed.join(', '));
return {
isValid: failed.length === 0,
validationError: failed.join(', '),
};
}
return { isValid: true, validationError: undefined };
}
/**
* @public
* @deprecated does not conform to the `errback` style followed by the rest of the library, and will be removed in the next major version. use `checkValidity` instead.
* @param {function(isValid: boolean, invalidReason: (string | undefined)): void} callback .
* @returns {void}
*/
valid(callback) {
const { isValid, validationError } = this.checkValidity();
callback(isValid, validationError);
}
/**
* @public
@ -623,6 +639,18 @@ class Message {
str.on('end', (err) => callback(err, buffer));
str.on('error', (err) => callback(err, buffer));
}
readAsync() {
return new Promise((resolve, reject) => {
this.read((err, buffer) => {
if (err != null) {
reject(err);
}
else {
resolve(buffer);
}
});
});
}
}
class MessageStream extends Stream {
/**
@ -701,10 +729,10 @@ class MessageStream extends Stream {
convertDashDelimitedTextToSnakeCase(header),
': ',
headers[header],
CRLF,
CRLF$1,
]);
}
output(data.concat([CRLF]).join(''));
output(data.concat([CRLF$1]).join(''));
};
/**
* @param {string} data the data to output as base64
@ -715,7 +743,7 @@ class MessageStream extends Stream {
const loops = Math.ceil(data.length / MIMECHUNK);
let loop = 0;
while (loop < loops) {
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF$1);
loop++;
}
if (callback) {
@ -723,54 +751,46 @@ class MessageStream extends Stream {
}
};
const outputFile = (attachment, next) => {
var _a;
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
const closed = (fd) => fs.closeSync(fd);
const inputEncoding = ((_a = attachment === null || attachment === void 0 ? void 0 : attachment.headers) === null || _a === void 0 ? void 0 : _a['content-transfer-encoding']) || 'base64';
const encoding = inputEncoding === '7bit'
? 'ascii'
: inputEncoding === '8bit'
? 'binary'
: inputEncoding;
/**
* @param {Error} err the error to emit
* @param {number} fd the file descriptor
* @returns {void}
*/
const opened = (err, fd) => {
if (!err) {
const read = (err, bytes) => {
if (!err && this.readable) {
let encoding = attachment && attachment.headers
? attachment.headers['content-transfer-encoding'] || 'base64'
: 'base64';
if (encoding === 'ascii' || encoding === '7bit') {
encoding = 'ascii';
}
else if (encoding === 'binary' || encoding === '8bit') {
encoding = 'binary';
}
else {
encoding = 'base64';
}
// guaranteed to be encoded without padding unless it is our last read
outputBase64(buffer.toString(encoding, 0, bytes), () => {
if (bytes == chunk) {
// we read a full chunk, there might be more
fs.read(fd, buffer, 0, chunk, null, read);
} // that was the last chunk, we are done reading the file
else {
this.removeListener('error', closed);
fs.close(fd, next);
}
});
}
else {
this.emit('error', err || { message: 'message stream was interrupted somehow!' });
}
};
fs.read(fd, buffer, 0, chunk, null, read);
this.once('error', closed);
}
else {
if (err) {
this.emit('error', err);
return;
}
const readBytes = (err, bytes) => {
if (err || this.readable === false) {
this.emit('error', err || new Error('message stream was interrupted somehow!'));
return;
}
// guaranteed to be encoded without padding unless it is our last read
outputBase64(buffer.toString(encoding, 0, bytes), () => {
if (bytes == chunk) {
// we read a full chunk, there might be more
read(fd, buffer, 0, chunk, null, readBytes);
} // that was the last chunk, we are done reading the file
else {
this.removeListener('error', closeSync);
close(fd, next);
}
});
};
read(fd, buffer, 0, chunk, null, readBytes);
this.once('error', closeSync);
};
fs.open(attachment.path, 'r', opened);
open(attachment.path, 'r', opened);
};
/**
* @param {MessageAttachment} attachment the metadata to use as headers
@ -829,7 +849,7 @@ class MessageStream extends Stream {
*/
const outputMessage = (boundary, list, index, callback) => {
if (index < list.length) {
output(`--${boundary}${CRLF}`);
output(`--${boundary}${CRLF$1}`);
if (list[index].related) {
outputRelated(list[index], () => outputMessage(boundary, list, index + 1, callback));
}
@ -838,21 +858,21 @@ class MessageStream extends Stream {
}
}
else {
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
output(`${CRLF$1}--${boundary}--${CRLF$1}${CRLF$1}`);
callback();
}
};
const outputMixed = () => {
const boundary = generateBoundary();
output(`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
output(`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
if (this.message.alternative == null) {
outputText(this.message);
outputMessage(boundary, this.message.attachments, 0, close);
outputMessage(boundary, this.message.attachments, 0, close$1);
}
else {
outputAlternative(
// typescript bug; should narrow to { alternative: MessageAttachment }
this.message, () => outputMessage(boundary, this.message.attachments, 0, close));
this.message, () => outputMessage(boundary, this.message.attachments, 0, close$1));
}
};
/**
@ -863,7 +883,8 @@ class MessageStream extends Stream {
const outputData = (attachment, callback) => {
var _a, _b;
outputBase64(attachment.encoded
? (_a = attachment.data) !== null && _a !== void 0 ? _a : '' : Buffer.from((_b = attachment.data) !== null && _b !== void 0 ? _b : '').toString('base64'), callback);
? (_a = attachment.data) !== null && _a !== void 0 ? _a : ''
: Buffer.from((_b = attachment.data) !== null && _b !== void 0 ? _b : '').toString('base64'), callback);
};
/**
* @param {Message} message the message to output
@ -874,12 +895,12 @@ class MessageStream extends Stream {
data = data.concat([
'Content-Type:',
message.content,
CRLF,
CRLF$1,
'Content-Transfer-Encoding: 7bit',
CRLF,
CRLF$1,
]);
data = data.concat(['Content-Disposition: inline', CRLF, CRLF]);
data = data.concat([message.text || '', CRLF, CRLF]);
data = data.concat(['Content-Disposition: inline', CRLF$1, CRLF$1]);
data = data.concat([message.text || '', CRLF$1, CRLF$1]);
output(data.join(''));
};
/**
@ -889,11 +910,11 @@ class MessageStream extends Stream {
*/
const outputRelated = (message, callback) => {
const boundary = generateBoundary();
output(`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
output(`Content-Type: multipart/related; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
outputAttachment(message, () => {
var _a;
outputMessage(boundary, (_a = message.related) !== null && _a !== void 0 ? _a : [], 0, () => {
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
output(`${CRLF$1}--${boundary}--${CRLF$1}${CRLF$1}`);
callback();
});
});
@ -905,14 +926,14 @@ class MessageStream extends Stream {
*/
const outputAlternative = (message, callback) => {
const boundary = generateBoundary();
output(`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
output(`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
outputText(message);
output(`--${boundary}${CRLF}`);
output(`--${boundary}${CRLF$1}`);
/**
* @returns {void}
*/
const finish = () => {
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
output([CRLF$1, '--', boundary, '--', CRLF$1, CRLF$1].join(''));
callback();
};
if (message.alternative.related) {
@ -922,7 +943,7 @@ class MessageStream extends Stream {
outputAttachment(message.alternative, finish);
}
};
const close = (err) => {
const close$1 = (err) => {
var _a, _b;
if (err) {
this.emit('error', err);
@ -945,12 +966,12 @@ class MessageStream extends Stream {
*/
const outputHeaderData = () => {
if (this.message.attachments.length || this.message.alternative) {
output(`MIME-Version: 1.0${CRLF}`);
output(`MIME-Version: 1.0${CRLF$1}`);
outputMixed();
} // you only have a text message!
else {
outputText(this.message);
close();
close$1();
}
};
/**
@ -966,14 +987,14 @@ class MessageStream extends Stream {
convertDashDelimitedTextToSnakeCase(header),
': ',
this.message.header[header],
CRLF,
CRLF$1,
]);
}
}
output(data.join(''));
outputHeaderData();
};
this.once('destroy', close);
this.once('destroy', close$1);
process.nextTick(outputHeader);
}
/**
@ -1141,7 +1162,7 @@ const DEFAULT_TIMEOUT = 5000;
const SMTP_PORT = 25;
const SMTP_SSL_PORT = 465;
const SMTP_TLS_PORT = 587;
const CRLF$1 = '\r\n';
const CRLF = '\r\n';
const GREYLIST_DELAY = 300;
let DEBUG = 0;
/**
@ -1228,7 +1249,7 @@ class SMTPConnection extends EventEmitter {
this.user = () => user;
this.password = () => password;
if (typeof logger === 'function') {
this.log = log;
this.log = logger;
}
}
/**
@ -1392,7 +1413,7 @@ class SMTPConnection extends EventEmitter {
this.greylistResponseTracker.has(response) === false) {
this.greylistResponseTracker.add(response);
setTimeout(() => {
this.send(cmd + CRLF$1, response);
this.send(cmd + CRLF, response);
}, GREYLIST_DELAY);
}
else {
@ -1403,7 +1424,7 @@ class SMTPConnection extends EventEmitter {
}
};
this.greylistResponseTracker.delete(response);
this.send(cmd + CRLF$1, response);
this.send(cmd + CRLF, response);
}
/**
* @public
@ -1575,7 +1596,7 @@ class SMTPConnection extends EventEmitter {
* @returns {void}
*/
data_end(callback) {
this.command(`${CRLF$1}.`, callback);
this.command(`${CRLF}.`, callback);
}
/**
* @public
@ -1686,8 +1707,8 @@ class SMTPConnection extends EventEmitter {
if (!method) {
const preferred = this.authentication;
let auth = '';
if (typeof ((_a = this.features) === null || _a === void 0 ? void 0 : _a.auth) === 'string') {
auth = this.features.auth;
if (typeof ((_a = this.features) === null || _a === void 0 ? void 0 : _a['auth']) === 'string') {
auth = this.features['auth'];
}
for (let i = 0; i < preferred.length; i++) {
if (auth.includes(preferred[i])) {
@ -1705,6 +1726,7 @@ class SMTPConnection extends EventEmitter {
const failed = (err, data) => {
this.loggedin = false;
this.close(); // if auth is bad, close the connection, it won't get better by itself
err.message = err.message.replace(this.password(), 'REDACTED');
caller(callback, SMTPError.create('authorization.failed', SMTPErrorStates.AUTHFAILED, err, data));
};
/**
@ -1832,8 +1854,9 @@ class SMTPClient {
}
/**
* @public
* @param {Message} msg the message to send
* @param {function(err: Error, msg: Message): void} callback .
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @param {MessageCallback<T>} callback receiver for the error (if any) as well as the passed-in message / headers
* @returns {void}
*/
send(msg, callback) {
@ -1846,33 +1869,35 @@ class SMTPClient {
callback(new Error('message is not a valid Message instance'), msg);
return;
}
message.valid((valid, why) => {
if (valid) {
const stack = this.createMessageStack(message, callback);
if (stack.to.length === 0) {
return callback(new Error('No recipients found in message'), msg);
}
this.queue.push(stack);
this._poll();
const { isValid, validationError } = message.checkValidity();
if (isValid) {
const stack = this.createMessageStack(message, callback);
if (stack.to.length === 0) {
return callback(new Error('No recipients found in message'), msg);
}
else {
callback(new Error(why), msg);
}
});
this.queue.push(stack);
this._poll();
}
else {
callback(new Error(validationError), msg);
}
}
/**
* @public
* @param {Message} msg the message to send
* @returns {Promise<Message>} a promise that resolves to the fully processed message
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @returns {Promise<T>} a promise that resolves to the passed-in message / headers
*/
sendAsync(msg) {
return new Promise((resolve, reject) => {
this.send(msg, (err, msg) => {
this.send(msg, (err, message) => {
if (err != null) {
reject(err);
}
else {
resolve(msg);
// unfortunately, the conditional type doesn't reach here
// fortunately, we only return a `Message` when err is null, so this is safe
resolve(message);
}
});
});
@ -1881,7 +1906,7 @@ class SMTPClient {
* @public
* @description Converts a message to the raw object used by the internal stack.
* @param {Message} message message to convert
* @param {function(err: Error, msg: Message): void} callback errback
* @param {MessageCallback} callback errback
* @returns {MessageStack} raw message object
*/
createMessageStack(message, callback = function () {
@ -2098,4 +2123,4 @@ class SMTPClient {
}
export { AUTH_METHODS, BUFFERSIZE, DEFAULT_TIMEOUT, MIME64CHUNK, MIMECHUNK, Message, SMTPClient, SMTPConnection, SMTPError, SMTPErrorStates, SMTPResponseMonitor, SMTPState, addressparser, getRFC2822Date, getRFC2822DateUTC, isRFC2822Date, mimeEncode, mimeWordEncode };
//# sourceMappingURL=email.mjs.map
//# sourceMappingURL=email.js.map

1
email.js.map Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
require('ts-node/register');
if (process.title === 'cjs') {
require('./rollup/email.cjs');
require.cache[require.resolve('./email.ts')] =
require.cache[require.resolve('./rollup/email.cjs')];
console.log('Testing email.cjs...\n');
}

View File

@ -1,8 +1,8 @@
export * from './smtp/address';
export * from './smtp/client';
export * from './smtp/connection';
export * from './smtp/date';
export * from './smtp/error';
export * from './smtp/message';
export * from './smtp/mime';
export * from './smtp/response';
export * from './smtp/address.js';
export * from './smtp/client.js';
export * from './smtp/connection.js';
export * from './smtp/date.js';
export * from './smtp/error.js';
export * from './smtp/message.js';
export * from './smtp/mime.js';
export * from './smtp/response.js';

View File

@ -1,7 +1,7 @@
{
"name": "emailjs",
"description": "send text/html emails and attachments (files, streams and strings) from node.js to any smtp server",
"version": "3.4.0",
"version": "4.0.3",
"author": "eleith",
"contributors": [
"izuzak",
@ -16,44 +16,53 @@
},
"type": "module",
"devDependencies": {
"@ledge/configs": "23.3.223",
"@rollup/plugin-typescript": "6.1.0",
"@types/mailparser": "3.0.0",
"@types/smtp-server": "3.5.5",
"@typescript-eslint/eslint-plugin": "4.8.2",
"@typescript-eslint/parser": "4.8.2",
"ava": "3.13.0",
"eslint": "7.14.0",
"eslint-config-prettier": "6.15.0",
"eslint-plugin-prettier": "3.1.4",
"mailparser": "3.0.1",
"prettier": "2.2.1",
"rollup": "2.34.0",
"smtp-server": "3.8.0",
"ts-node": "9.0.0",
"tslib": "2.0.3",
"typescript": "4.1.2"
"@ledge/configs": "23.3.23322",
"@rollup/plugin-typescript": "8.3.2",
"@types/mailparser": "3.4.0",
"@types/node": "12.12.6",
"@types/smtp-server": "3.5.7",
"@typescript-eslint/eslint-plugin": "5.21.0",
"@typescript-eslint/parser": "5.21.0",
"ava": "4.2.0",
"eslint": "8.14.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.0.0",
"mailparser": "3.5.0",
"prettier": "2.6.2",
"rollup": "2.70.2",
"smtp-server": "3.11.0",
"ts-node": "10.9.1",
"tslib": "2.4.0",
"typescript": "4.3.5"
},
"peerDependencies": {
"typescript": ">=4.3.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"resolutions": {
"nodemailer": "6.7.4"
},
"engines": {
"node": ">=12"
},
"engine": [
"node >= 10"
],
"files": [
"email.js",
"email.ts",
"smtp",
"rollup"
"smtp"
],
"main": "./rollup/email.cjs",
"types": "./email.ts",
"exports": {
"import": "./rollup/email.mjs",
"require": "./rollup/email.cjs"
"default": "./email.js"
},
"scripts": {
"build": "rollup -c rollup.config.ts",
"lint": "eslint *.ts \"+(smtp|test)/*.ts\"",
"tsc": "tsc",
"test": "ava",
"test-cjs": "npm run build && npm run test -- --node-arguments='--title=cjs'"
"pretest": "yarn build",
"test": "ava"
},
"license": "MIT"
}

View File

@ -1,23 +1,15 @@
import module from 'module';
import { builtinModules } from 'module';
import typescript from '@rollup/plugin-typescript';
export default {
input: 'email.ts',
output: [
{
file: 'rollup/email.cjs',
format: 'cjs',
interop: 'default',
sourcemap: true,
},
{
file: 'rollup/email.mjs',
format: 'es',
sourcemap: true,
},
],
external: module.builtinModules,
output: {
file: 'email.js',
format: 'es',
sourcemap: true,
},
external: builtinModules,
plugins: [
typescript({ removeComments: false, include: ['email.ts', 'smtp/**/*'] }),
typescript({ removeComments: false, include: ['email.ts', 'smtp/*'] }),
],
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,19 @@
import { addressparser } from './address';
import { Message, MessageAttachment, MessageHeaders } from './message';
import { SMTPConnection, SMTPConnectionOptions, SMTPState } from './connection';
import { addressparser } from './address.js';
import type { MessageAttachment, MessageHeaders } from './message.js';
import { Message } from './message.js';
import type { SMTPConnectionOptions } from './connection.js';
import { SMTPConnection, SMTPState } from './connection.js';
export type MessageCallback<T = Message | MessageHeaders> = <
U extends Error | null,
V extends U extends Error ? T : Message
>(
err: U,
msg: V
) => void;
export interface MessageStack {
callback: (error: Error | null, message: Message) => void;
callback: MessageCallback;
message: Message;
attachment: MessageAttachment;
text: string;
@ -20,7 +30,7 @@ export class SMTPClient {
protected sending = false;
protected ready = false;
protected timer: NodeJS.Timer | null = null;
protected timer: NodeJS.Timeout | null = null;
/**
* Create a standard SMTP client backed by a self-managed SMTP connection.
@ -35,15 +45,16 @@ export class SMTPClient {
/**
* @public
* @param {Message} msg the message to send
* @param {function(err: Error, msg: Message): void} callback .
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @param {MessageCallback<T>} callback receiver for the error (if any) as well as the passed-in message / headers
* @returns {void}
*/
public send(
msg: Message,
callback: (err: Error | null, msg: Message) => void
) {
const message: Message | null =
public send<T extends Message | MessageHeaders>(
msg: T,
callback: MessageCallback<T>
): void {
const message =
msg instanceof Message
? msg
: this._canMakeMessage(msg)
@ -55,32 +66,35 @@ export class SMTPClient {
return;
}
message.valid((valid, why) => {
if (valid) {
const stack = this.createMessageStack(message, callback);
if (stack.to.length === 0) {
return callback(new Error('No recipients found in message'), msg);
}
this.queue.push(stack);
this._poll();
} else {
callback(new Error(why), msg);
const { isValid, validationError } = message.checkValidity();
if (isValid) {
const stack = this.createMessageStack(message, callback);
if (stack.to.length === 0) {
return callback(new Error('No recipients found in message'), msg);
}
});
this.queue.push(stack);
this._poll();
} else {
callback(new Error(validationError), msg);
}
}
/**
* @public
* @param {Message} msg the message to send
* @returns {Promise<Message>} a promise that resolves to the fully processed message
* @template {Message | MessageHeaders} T
* @param {T} msg the message to send
* @returns {Promise<T>} a promise that resolves to the passed-in message / headers
*/
public sendAsync(msg: Message) {
public sendAsync<T extends Message | MessageHeaders>(msg: T) {
return new Promise<Message>((resolve, reject) => {
this.send(msg, (err, msg) => {
this.send(msg, (err, message) => {
if (err != null) {
reject(err);
} else {
resolve(msg);
// unfortunately, the conditional type doesn't reach here
// fortunately, we only return a `Message` when err is null, so this is safe
resolve(message as Message);
}
});
});
@ -90,12 +104,12 @@ export class SMTPClient {
* @public
* @description Converts a message to the raw object used by the internal stack.
* @param {Message} message message to convert
* @param {function(err: Error, msg: Message): void} callback errback
* @param {MessageCallback} callback errback
* @returns {MessageStack} raw message object
*/
public createMessageStack(
message: Message,
callback: (err: Error | null, msg: Message) => void = function () {
callback: MessageCallback = function () {
/* ø */
}
) {
@ -231,7 +245,7 @@ export class SMTPClient {
* @returns {boolean} whether the attachment contains inlined html
*/
protected _containsInlinedHtml(
attachment: MessageAttachment | MessageAttachment[]
attachment?: MessageAttachment | MessageAttachment[]
) {
if (Array.isArray(attachment)) {
return attachment.some((att) => {
@ -247,7 +261,7 @@ export class SMTPClient {
* @param {MessageAttachment} attachment attachment
* @returns {boolean} whether the attachment is inlined html
*/
protected _isAttachmentInlinedHtml(attachment: MessageAttachment) {
protected _isAttachmentInlinedHtml(attachment?: MessageAttachment) {
return (
attachment &&
(attachment.data || attachment.path) &&

View File

@ -2,15 +2,11 @@ import { createHmac } from 'crypto';
import { EventEmitter } from 'events';
import { Socket } from 'net';
import { hostname } from 'os';
import {
connect,
createSecureContext,
ConnectionOptions,
TLSSocket,
} from 'tls';
import { connect, createSecureContext, TLSSocket } from 'tls';
import type { ConnectionOptions } from 'tls';
import { SMTPError, SMTPErrorStates } from './error';
import { SMTPResponseMonitor } from './response';
import { SMTPError, SMTPErrorStates } from './error.js';
import { SMTPResponseMonitor } from './response.js';
/**
* @readonly
@ -187,7 +183,7 @@ export class SMTPConnection extends EventEmitter {
this.password = () => password as string;
if (typeof logger === 'function') {
this.log = log;
this.log = logger;
}
}
@ -762,8 +758,8 @@ export class SMTPConnection extends EventEmitter {
const preferred = this.authentication;
let auth = '';
if (typeof this.features?.auth === 'string') {
auth = this.features.auth;
if (typeof this.features?.['auth'] === 'string') {
auth = this.features['auth'];
}
for (let i = 0; i < preferred.length; i++) {
@ -783,6 +779,9 @@ export class SMTPConnection extends EventEmitter {
const failed = (err: Error, data: unknown) => {
this.loggedin = false;
this.close(); // if auth is bad, close the connection, it won't get better by itself
err.message = err.message.replace(this.password(), 'REDACTED');
caller(
callback,
SMTPError.create(

View File

@ -39,7 +39,8 @@ export function getRFC2822DateUTC(date = new Date()) {
* @see https://tools.ietf.org/html/rfc2822#section-3.3
* @see https://github.com/moment/moment/blob/a831fc7e2694281ce31e4f090bbcf90a690f0277/src/lib/create/from-string.js#L101
*/
const rfc2822re = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/.compile();
const rfc2822re =
/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;
/**
* @param {string} [date] a string to check for conformance to the [rfc2822](https://tools.ietf.org/html/rfc2822#section-3.3) standard

View File

@ -1,11 +1,18 @@
import fs, { PathLike } from 'fs';
import type { PathLike } from 'fs';
import {
existsSync,
open as openFile,
close as closeFile,
closeSync as closeFileSync,
read as readFile,
} from 'fs';
import { hostname } from 'os';
import { Stream } from 'stream';
import type { Readable } from 'stream';
import { addressparser } from './address';
import { getRFC2822Date } from './date';
import { mimeWordEncode } from './mime';
import { addressparser } from './address.js';
import { getRFC2822Date } from './date.js';
import { mimeWordEncode } from './mime.js';
const CRLF = '\r\n' as const;
@ -61,19 +68,20 @@ export interface MessageHeaders {
| string
| string[]
| null
| undefined
| MessageAttachment
| MessageAttachment[];
'content-type': string;
'message-id': string;
'return-path': string | null;
date: string;
'content-type'?: string;
'message-id'?: string;
'return-path'?: string | null;
date?: string;
from: string | string[];
to: string | string[];
cc: string | string[];
bcc: string | string[];
cc?: string | string[];
bcc?: string | string[];
subject: string;
text: string | null;
attachment: MessageAttachment | MessageAttachment[];
attachment?: MessageAttachment | MessageAttachment[];
}
let counter = 0;
@ -186,16 +194,20 @@ export class Message {
/**
* @public
* @param {function(isValid: boolean, invalidReason: string): void} callback .
* @returns {void}
* @returns {{ isValid: boolean, validationError: (string | undefined) }} an object specifying whether this message is validly formatted, and the first validation error if it is not.
*/
public valid(callback: (isValid: boolean, invalidReason?: string) => void) {
public checkValidity() {
if (
typeof this.header.from !== 'string' &&
Array.isArray(this.header.from) === false
) {
callback(false, 'Message must have a `from` header');
} else if (
return {
isValid: false,
validationError: 'Message must have a `from` header',
};
}
if (
typeof this.header.to !== 'string' &&
Array.isArray(this.header.to) === false &&
typeof this.header.cc !== 'string' &&
@ -203,18 +215,19 @@ export class Message {
typeof this.header.bcc !== 'string' &&
Array.isArray(this.header.bcc) === false
) {
callback(
false,
'Message must have at least one `to`, `cc`, or `bcc` header'
);
} else if (this.attachments.length === 0) {
callback(true, undefined);
} else {
return {
isValid: false,
validationError:
'Message must have at least one `to`, `cc`, or `bcc` header',
};
}
if (this.attachments.length > 0) {
const failed: string[] = [];
this.attachments.forEach((attachment) => {
if (attachment.path) {
if (fs.existsSync(attachment.path) == false) {
if (existsSync(attachment.path) === false) {
failed.push(`${attachment.path} does not exist`);
}
} else if (attachment.stream) {
@ -225,9 +238,24 @@ export class Message {
failed.push('attachment has no data associated with it');
}
});
callback(failed.length === 0, failed.join(', '));
return {
isValid: failed.length === 0,
validationError: failed.join(', '),
};
}
return { isValid: true, validationError: undefined };
}
/**
* @public
* @deprecated does not conform to the `errback` style followed by the rest of the library, and will be removed in the next major version. use `checkValidity` instead.
* @param {function(isValid: boolean, invalidReason: (string | undefined)): void} callback .
* @returns {void}
*/
public valid(callback: (isValid: boolean, invalidReason?: string) => void) {
const { isValid, validationError } = this.checkValidity();
callback(isValid, validationError);
}
/**
@ -250,6 +278,18 @@ export class Message {
str.on('end', (err) => callback(err, buffer));
str.on('error', (err) => callback(err, buffer));
}
public readAsync() {
return new Promise<string>((resolve, reject) => {
this.read((err, buffer) => {
if (err != null) {
reject(err);
} else {
resolve(buffer);
}
});
});
}
}
class MessageStream extends Stream {
@ -379,7 +419,15 @@ class MessageStream extends Stream {
) => {
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
const closed = (fd: number) => fs.closeSync(fd);
const inputEncoding =
attachment?.headers?.['content-transfer-encoding'] || 'base64';
const encoding =
inputEncoding === '7bit'
? 'ascii'
: inputEncoding === '8bit'
? 'binary'
: inputEncoding;
/**
* @param {Error} err the error to emit
@ -387,47 +435,38 @@ class MessageStream extends Stream {
* @returns {void}
*/
const opened = (err: NodeJS.ErrnoException | null, fd: number) => {
if (!err) {
const read = (err: NodeJS.ErrnoException | null, bytes: number) => {
if (!err && this.readable) {
let encoding =
attachment && attachment.headers
? attachment.headers['content-transfer-encoding'] || 'base64'
: 'base64';
if (encoding === 'ascii' || encoding === '7bit') {
encoding = 'ascii';
} else if (encoding === 'binary' || encoding === '8bit') {
encoding = 'binary';
} else {
encoding = 'base64';
}
// guaranteed to be encoded without padding unless it is our last read
outputBase64(buffer.toString(encoding, 0, bytes), () => {
if (bytes == chunk) {
// we read a full chunk, there might be more
fs.read(fd, buffer, 0, chunk, null, read);
} // that was the last chunk, we are done reading the file
else {
this.removeListener('error', closed);
fs.close(fd, next);
}
});
} else {
this.emit(
'error',
err || { message: 'message stream was interrupted somehow!' }
);
}
};
fs.read(fd, buffer, 0, chunk, null, read);
this.once('error', closed);
} else {
if (err) {
this.emit('error', err);
return;
}
const readBytes = (
err: NodeJS.ErrnoException | null,
bytes: number
) => {
if (err || this.readable === false) {
this.emit(
'error',
err || new Error('message stream was interrupted somehow!')
);
return;
}
// guaranteed to be encoded without padding unless it is our last read
outputBase64(buffer.toString(encoding, 0, bytes), () => {
if (bytes == chunk) {
// we read a full chunk, there might be more
readFile(fd, buffer, 0, chunk, null, readBytes);
} // that was the last chunk, we are done reading the file
else {
this.removeListener('error', closeFileSync);
closeFile(fd, next);
}
});
};
readFile(fd, buffer, 0, chunk, null, readBytes);
this.once('error', closeFileSync);
};
fs.open(attachment.path as PathLike, 'r', opened);
openFile(attachment.path as PathLike, 'r', opened);
};
/**

View File

@ -13,9 +13,8 @@ const RANGES = [
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
[0x3e, 0x7e], // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
];
const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(
''
);
const LOOKUP =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
const MAX_CHUNK_LENGTH = 16383; // must be multiple of 3
const MAX_MIME_WORD_LENGTH = 52;
const MAX_B64_MIME_WORD_BYTE_LENGTH = 39;

View File

@ -1,4 +1,4 @@
import { SMTPError, SMTPErrorStates } from './error';
import { SMTPError, SMTPErrorStates } from './error.js';
import type { Socket } from 'net';
import type { TLSSocket } from 'tls';

View File

@ -1,5 +1,5 @@
import test from 'ava';
import { addressparser } from '../email';
import { addressparser } from '../email.js';
test('addressparser should handle single address correctly', async (t) => {
t.deepEqual(addressparser('andris@tr.ee'), [

View File

@ -1,8 +1,10 @@
import test, { ExecutionContext } from 'ava';
import test from 'ava';
import type { ExecutionContext } from 'ava';
import { simpleParser } from 'mailparser';
import type { AddressObject } from 'mailparser';
import { SMTPServer } from 'smtp-server';
import { AUTH_METHODS, SMTPClient, Message } from '../email';
import { AUTH_METHODS, SMTPClient, Message } from '../email.js';
let port = 2000;
@ -12,15 +14,15 @@ function send(
authMethods = [],
authOptional = false,
secure = false,
password = 'honey',
}: {
authMethods?: (keyof typeof AUTH_METHODS)[];
authOptional?: boolean;
secure?: boolean;
password?: string;
} = {}
) {
return new Promise<void>((resolve, reject) => {
t.plan(5);
const msg = {
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
@ -42,9 +44,14 @@ function send(
? accessToken === 'honey'
: password === 'honey')
) {
t.plan(5);
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
return callback(
new Error(
`invalid user or pass: ${username || accessToken} ${password}`
)
);
}
},
async onData(stream, _session, callback: () => void) {
@ -57,7 +64,7 @@ function send(
t.is(mail.text, msg.text + '\n\n\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
t.is(mail.to?.text, msg.to);
t.is((mail.to as AddressObject).text, msg.to);
callback();
},
@ -66,12 +73,12 @@ function send(
server.listen(p, () => {
const options = Object.assign(
{ port: p, ssl: secure, authentication: authMethods },
authOptional ? {} : { user: 'pooh', password: 'honey' }
authOptional ? {} : { user: 'pooh', password }
);
new SMTPClient(options).send(new Message(msg), (err) => {
server.close(() => {
if (err) {
reject(err.message);
reject(err);
} else {
resolve();
}
@ -118,3 +125,16 @@ test('XOAUTH2 authentication (encrypted) should succeed', async (t) => {
send(t, { authMethods: [AUTH_METHODS.XOAUTH2], secure: true })
);
});
test('on authentication.failed error message should not contain password', async (t) => {
t.plan(1);
const password = 'passpot';
await send(t, {
authMethods: [AUTH_METHODS.LOGIN],
secure: true,
password,
}).catch((err) => {
t.false(err.message.includes(password));
});
});

View File

@ -1,20 +1,21 @@
import { promisify } from 'util';
import test from 'ava';
import { simpleParser, ParsedMail } from 'mailparser';
import { simpleParser } from 'mailparser';
import type { ParsedMail, AddressObject } from 'mailparser';
import { SMTPServer } from 'smtp-server';
import type { MessageHeaders } from '../email.js';
import {
DEFAULT_TIMEOUT,
SMTPClient,
Message,
MessageHeaders,
isRFC2822Date,
} from '../email';
} from '../email.js';
const parseMap = new Map<string, ParsedMail>();
const port = 3000;
let greylistPort = 4000;
const port = 3333;
let greylistPort = 4444;
const client = new SMTPClient({
port,
@ -72,28 +73,23 @@ test('client invokes callback exactly once for invalid connection', async (t) =>
await t.notThrowsAsync(
new Promise<void>((resolve, reject) => {
let counter = 0;
const invalidClient = new SMTPClient({ host: 'bar.baz' });
const incrementListener = () => {
const invalidClient = new SMTPClient({ host: 'localhost' });
const incrementCounter = () => {
if (counter > 0) {
reject();
} else {
counter++;
}
};
invalidClient.smtp.addListener('incrementTestCounter', incrementListener);
invalidClient.send(new Message(msg), (err) => {
if (err == null || counter > 0) {
if (err == null) {
reject();
} else {
invalidClient.smtp.emit('incrementTestCounter');
incrementCounter();
}
});
// @ts-expect-error the error event is only accessible from the protected socket property
invalidClient.smtp.sock.once('error', () => {
invalidClient.smtp.removeListener(
'incrementTestCounter',
incrementListener
);
if (counter === 1) {
resolve();
} else {
@ -145,7 +141,7 @@ test('client accepts array recipients', async (t) => {
msg.header.cc = [msg.header.cc as string];
msg.header.bcc = [msg.header.bcc as string];
const isValid = await new Promise((r) => msg.valid(r));
const { isValid } = msg.checkValidity();
const stack = client.createMessageStack(msg);
t.true(isValid);
@ -163,29 +159,32 @@ test('client accepts array sender', async (t) => {
});
msg.header.from = [msg.header.from as string];
const isValid = await new Promise((r) => msg.valid(r));
const { isValid } = msg.checkValidity();
t.true(isValid);
});
test('client rejects message without `from` header', async (t) => {
const { message: error } = await t.throwsAsync(
const error = await t.throwsAsync(
send({
subject: 'this is a test TEXT message from emailjs',
text: "It is hard to be brave when you're only a Very Small Animal.",
})
);
t.is(error, 'Message must have a `from` header');
t.is(error?.message, 'Message must have a `from` header');
});
test('client rejects message without `to`, `cc`, or `bcc` header', async (t) => {
const { message: error } = await t.throwsAsync(
const error = await t.throwsAsync(
send({
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
text: "It is hard to be brave when you're only a Very Small Animal.",
})
);
t.is(error, 'Message must have at least one `to`, `cc`, or `bcc` header');
t.is(
error?.message,
'Message must have at least one `to`, `cc`, or `bcc` header'
);
});
test('client allows message with only `cc` recipient header', async (t) => {
@ -200,7 +199,7 @@ test('client allows message with only `cc` recipient header', async (t) => {
t.is(mail.text, msg.text + '\n\n\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
t.is(mail.cc?.text, msg.cc);
t.is((mail.cc as AddressObject).text, msg.cc);
});
test('client allows message with only `bcc` recipient header', async (t) => {
@ -260,13 +259,13 @@ test('client supports greylisting', async (t) => {
greylistServer.onRcptTo = (a, s, cb) => {
t.pass();
const err = new Error('greylist');
((err as never) as { responseCode: number }).responseCode = 450;
(err as never as { responseCode: number }).responseCode = 450;
greylistServer.onRcptTo = onRcptTo;
onRcptTo(a, s, cb);
};
const err = new Error('greylist');
((err as never) as { responseCode: number }).responseCode = 450;
(err as never as { responseCode: number }).responseCode = 450;
callback(err);
};
@ -307,7 +306,7 @@ test('client only responds once to greylisting', async (t) => {
onRcptTo(_address, _session, callback) {
t.pass();
const err = new Error('greylist');
((err as never) as { responseCode: number }).responseCode = 450;
(err as never as { responseCode: number }).responseCode = 450;
callback(err);
},
onAuth(auth, _session, callback) {
@ -320,7 +319,7 @@ test('client only responds once to greylisting', async (t) => {
});
const p = greylistPort++;
const { message: error } = await t.throwsAsync(
const error = await t.throwsAsync(
new Promise<void>((resolve, reject) => {
greylistServer.listen(p, () => {
new SMTPClient({
@ -339,7 +338,7 @@ test('client only responds once to greylisting', async (t) => {
});
})
);
t.is(error, "bad response on command 'RCPT': greylist");
t.is(error?.message, "bad response on command 'RCPT': greylist");
});
test('client send can have result awaited when promisified', async (t) => {
@ -354,7 +353,7 @@ test('client send can have result awaited when promisified', async (t) => {
};
try {
const message = await sendAsync(new Message(msg));
const message = (await sendAsync(new Message(msg))) as Message;
t.true(message instanceof Message);
t.like(message, {
alternative: null,
@ -370,7 +369,13 @@ test('client send can have result awaited when promisified', async (t) => {
t.true(isRFC2822Date(message.header.date as string));
t.regex(message.header['message-id'] as string, /^<.*[@]{1}.*>$/);
} catch (err) {
t.fail(err);
if (err instanceof Error) {
t.fail(err.message);
} else if (typeof err === 'string') {
t.fail(err);
} else {
t.fail();
}
}
});
@ -399,7 +404,13 @@ test('client sendAsync can have result awaited', async (t) => {
t.true(isRFC2822Date(message.header.date as string));
t.regex(message.header['message-id'] as string, /^<.*[@]{1}.*>$/);
} catch (err) {
t.fail(err);
if (err instanceof Error) {
t.fail(err.message);
} else if (typeof err === 'string') {
t.fail(err);
} else {
t.fail();
}
}
});
@ -412,7 +423,7 @@ test('client sendAsync can have error caught when awaited', async (t) => {
};
try {
const invalidClient = new SMTPClient({ host: 'bar.baz' });
const invalidClient = new SMTPClient({ host: '127.0.0.1' });
const message = await invalidClient.sendAsync(new Message(msg));
t.true(message instanceof Message);
t.fail();

11
test/connection.ts Normal file
View File

@ -0,0 +1,11 @@
import test from 'ava';
import { SMTPConnection } from '../email.js';
test('accepts a custom logger', async (t) => {
const logger = () => {
/** ø */
};
const connection = new SMTPConnection({ logger });
t.is(Reflect.get(connection, 'log'), logger);
});

View File

@ -1,5 +1,5 @@
import test from 'ava';
import { getRFC2822Date, getRFC2822DateUTC, isRFC2822Date } from '../email';
import { getRFC2822Date, getRFC2822DateUTC, isRFC2822Date } from '../email.js';
const toD_utc = (dt: number) => getRFC2822DateUTC(new Date(dt));
const toD = (dt: number, utc = false) => getRFC2822Date(new Date(dt), utc);

View File

@ -1,15 +1,37 @@
import { readFileSync, createReadStream } from 'fs';
import { join } from 'path';
import { createReadStream, readFileSync } from 'fs';
import { URL } from 'url';
import test from 'ava';
import { simpleParser, ParsedMail } from 'mailparser';
import { simpleParser } from 'mailparser';
import type { AddressObject, ParsedMail } from 'mailparser';
import { SMTPServer } from 'smtp-server';
import { SMTPClient, Message, MessageAttachment } from '../email';
import { MessageHeaders } from '../smtp/message';
import { SMTPClient, Message } from '../email.js';
import type { MessageAttachment, MessageHeaders } from '../email.js';
const port = 5000;
const parseMap = new Map<string, ParsedMail>();
const textFixtureUrl = new URL('attachments/smtp.txt', import.meta.url);
const textFixture = readFileSync(textFixtureUrl, 'utf-8');
const htmlFixtureUrl = new URL('attachments/smtp.html', import.meta.url);
const htmlFixture = readFileSync(htmlFixtureUrl, 'utf-8');
const pdfFixtureUrl = new URL('attachments/smtp.pdf', import.meta.url);
const pdfFixture = readFileSync(pdfFixtureUrl, 'base64');
const tarFixtureUrl = new URL(
'attachments/postfix-2.8.7.tar.gz',
import.meta.url
);
const tarFixture = readFileSync(tarFixtureUrl, 'base64');
/**
* \@types/mailparser@3.0.2 breaks our code
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50744
*/
type ParsedMailCompat = Omit<ParsedMail, 'to'> & { to?: AddressObject };
const port = 5555;
const parseMap = new Map<string, ParsedMailCompat>();
const client = new SMTPClient({
port,
@ -27,11 +49,11 @@ const server = new SMTPServer({
}
},
async onData(stream, _session, callback: () => void) {
const mail = await simpleParser(stream, {
const mail = (await simpleParser(stream, {
skipHtmlToText: true,
skipTextToHtml: true,
skipImageLinks: true,
} as Record<string, unknown>);
} as Record<string, unknown>)) as ParsedMailCompat;
parseMap.set(mail.subject as string, mail);
callback();
@ -39,37 +61,12 @@ const server = new SMTPServer({
});
function send(headers: Partial<MessageHeaders>) {
return new Promise<ParsedMail>((resolve, reject) => {
return new Promise<ParsedMailCompat>((resolve, reject) => {
client.send(new Message(headers), (err) => {
if (err) {
reject(err);
} else {
resolve(parseMap.get(headers.subject as string) as ParsedMail);
}
});
});
}
function validate(headers: Partial<MessageHeaders>) {
const { to, cc, bcc } = headers;
const msg = new Message(headers);
if (Array.isArray(to)) {
msg.header.to = to;
}
if (Array.isArray(cc)) {
msg.header.to = to;
}
if (Array.isArray(bcc)) {
msg.header.to = to;
}
return new Promise((resolve, reject) => {
msg.valid((isValid, reason) => {
if (isValid) {
resolve(isValid);
} else {
reject(new Error(reason));
resolve(parseMap.get(headers.subject as string) as ParsedMailCompat);
}
});
});
@ -148,7 +145,7 @@ test('very large text message', async (t) => {
subject: 'this is a test TEXT message from emailjs',
from: 'ninjas@gmail.com',
to: 'pirates@gmail.com',
text: readFileSync(join(__dirname, 'attachments/smtp.txt'), 'utf-8'),
text: textFixture,
};
const mail = await send(msg);
@ -159,17 +156,13 @@ test('very large text message', async (t) => {
});
test('very large text data message', async (t) => {
const text =
'<html><body><pre>' +
readFileSync(join(__dirname, 'attachments/smtp.txt'), 'utf-8') +
'</pre></body></html>';
const text = '<html><body><pre>' + textFixture + '</pre></body></html>';
const msg = {
subject: 'this is a test TEXT+DATA message from emailjs',
from: 'lobsters@gmail.com',
to: 'lizards@gmail.com',
text:
'hello friend if you are seeing this, you can not view html emails. it is attached inline.',
text: 'hello friend if you are seeing this, you can not view html emails. it is attached inline.',
attachment: {
data: text,
alternative: true,
@ -185,19 +178,18 @@ test('very large text data message', async (t) => {
});
test('html data message', async (t) => {
const html = readFileSync(join(__dirname, 'attachments/smtp.html'), 'utf-8');
const msg = {
subject: 'this is a test TEXT+HTML+DATA message from emailjs',
from: 'obama@gmail.com',
to: 'mitt@gmail.com',
attachment: {
data: html,
data: htmlFixture,
alternative: true,
},
};
const mail = await send(msg);
t.is(mail.html, html.replace(/\r/g, ''));
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -205,19 +197,18 @@ test('html data message', async (t) => {
});
test('html file message', async (t) => {
const html = readFileSync(join(__dirname, 'attachments/smtp.html'), 'utf-8');
const msg = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: {
path: join(__dirname, 'attachments/smtp.html'),
path: new URL('attachments/smtp.html', import.meta.url),
alternative: true,
},
};
const mail = await send(msg);
t.is(mail.html, html.replace(/\r/g, ''));
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -225,18 +216,18 @@ test('html file message', async (t) => {
});
test('html with image embed message', async (t) => {
const html = readFileSync(join(__dirname, 'attachments/smtp2.html'), 'utf-8');
const image = readFileSync(join(__dirname, 'attachments/smtp.gif'));
const htmlFixture2Url = new URL('attachments/smtp2.html', import.meta.url);
const imageFixtureUrl = new URL('attachments/smtp.gif', import.meta.url);
const msg = {
subject: 'this is a test TEXT+HTML+IMAGE message from emailjs',
from: 'ninja@gmail.com',
to: 'pirate@gmail.com',
attachment: {
path: join(__dirname, 'attachments/smtp2.html'),
path: htmlFixture2Url,
alternative: true,
related: [
{
path: join(__dirname, 'attachments/smtp.gif'),
path: imageFixtureUrl,
type: 'image/gif',
name: 'smtp-diagram.gif',
headers: { 'Content-ID': '<smtp-diagram@local>' },
@ -248,9 +239,9 @@ test('html with image embed message', async (t) => {
const mail = await send(msg);
t.is(
mail.attachments[0].content.toString('base64'),
image.toString('base64')
readFileSync(imageFixtureUrl, 'base64')
);
t.is(mail.html, html.replace(/\r/g, ''));
t.is(mail.html, readFileSync(htmlFixture2Url, 'utf-8').replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -258,19 +249,21 @@ test('html with image embed message', async (t) => {
});
test('html data and attachment message', async (t) => {
const html = readFileSync(join(__dirname, 'attachments/smtp.html'), 'utf-8');
const msg = {
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
from: 'thomas@gmail.com',
to: 'nikolas@gmail.com',
attachment: [
{ path: join(__dirname, 'attachments/smtp.html'), alternative: true },
{ path: join(__dirname, 'attachments/smtp.gif') },
{
path: new URL('attachments/smtp.html', import.meta.url),
alternative: true,
},
{ path: new URL('attachments/smtp.gif', import.meta.url) },
] as MessageAttachment[],
};
const mail = await send(msg);
t.is(mail.html, html.replace(/\r/g, ''));
t.is(mail.html, htmlFixture.replace(/\r/g, ''));
t.is(mail.text, '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -278,21 +271,20 @@ test('html data and attachment message', async (t) => {
});
test('attachment message', async (t) => {
const pdf = readFileSync(join(__dirname, 'attachments/smtp.pdf'));
const msg = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: join(__dirname, 'attachments/smtp.pdf'),
path: pdfFixtureUrl,
type: 'application/pdf',
name: 'smtp-info.pdf',
} as MessageAttachment,
};
const mail = await send(msg);
t.is(mail.attachments[0].content.toString('base64'), pdf.toString('base64'));
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -300,21 +292,20 @@ test('attachment message', async (t) => {
});
test('attachment sent with unicode filename message', async (t) => {
const pdf = readFileSync(join(__dirname, 'attachments/smtp.pdf'));
const msg = {
subject: 'this is a test TEXT+ATTACHMENT message from emailjs',
from: 'washing@gmail.com',
to: 'lincoln@gmail.com',
text: 'hello friend, i hope this message and pdf finds you well.',
attachment: {
path: join(__dirname, 'attachments/smtp.pdf'),
path: pdfFixtureUrl,
type: 'application/pdf',
name: 'smtp-✓-info.pdf',
} as MessageAttachment,
};
const mail = await send(msg);
t.is(mail.attachments[0].content.toString('base64'), pdf.toString('base64'));
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[0].filename, 'smtp-✓-info.pdf');
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
@ -323,8 +314,6 @@ test('attachment sent with unicode filename message', async (t) => {
});
test('attachments message', async (t) => {
const pdf = readFileSync(join(__dirname, 'attachments/smtp.pdf'));
const tar = readFileSync(join(__dirname, 'attachments/postfix-2.8.7.tar.gz'));
const msg = {
subject: 'this is a test TEXT+2+ATTACHMENTS message from emailjs',
from: 'sergey@gmail.com',
@ -332,12 +321,12 @@ test('attachments message', async (t) => {
text: 'hello friend, i hope this message and attachments finds you well.',
attachment: [
{
path: join(__dirname, 'attachments/smtp.pdf'),
path: pdfFixtureUrl,
type: 'application/pdf',
name: 'smtp-info.pdf',
},
{
path: join(__dirname, 'attachments/postfix-2.8.7.tar.gz'),
path: tarFixtureUrl,
type: 'application/tar-gz',
name: 'postfix.source.2.8.7.tar.gz',
},
@ -345,8 +334,8 @@ test('attachments message', async (t) => {
};
const mail = await send(msg);
t.is(mail.attachments[0].content.toString('base64'), pdf.toString('base64'));
t.is(mail.attachments[1].content.toString('base64'), tar.toString('base64'));
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[1].content.toString('base64'), tarFixture);
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -354,35 +343,32 @@ test('attachments message', async (t) => {
});
test('streams message', async (t) => {
const pdf = readFileSync(join(__dirname, 'attachments/smtp.pdf'));
const tar = readFileSync(join(__dirname, 'attachments/postfix-2.8.7.tar.gz'));
const stream = createReadStream(join(__dirname, 'attachments/smtp.pdf'));
const stream2 = createReadStream(
join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
);
const msg = {
subject: 'this is a test TEXT+2+STREAMED+ATTACHMENTS message from emailjs',
from: 'stanford@gmail.com',
to: 'mit@gmail.com',
text:
'hello friend, i hope this message and streamed attachments finds you well.',
text: 'hello friend, i hope this message and streamed attachments finds you well.',
attachment: [
{ stream, type: 'application/pdf', name: 'smtp-info.pdf' },
{
stream: stream2,
stream: createReadStream(pdfFixtureUrl),
type: 'application/pdf',
name: 'smtp-info.pdf',
},
{
stream: createReadStream(tarFixtureUrl),
type: 'application/x-gzip',
name: 'postfix.source.2.8.7.tar.gz',
},
],
};
stream.pause();
stream2.pause();
for (const { stream } of msg.attachment) {
stream.pause();
}
const mail = await send(msg);
t.is(mail.attachments[0].content.toString('base64'), pdf.toString('base64'));
t.is(mail.attachments[1].content.toString('base64'), tar.toString('base64'));
t.is(mail.attachments[0].content.toString('base64'), pdfFixture);
t.is(mail.attachments[1].content.toString('base64'), tarFixture);
t.is(mail.text, msg.text + '\n');
t.is(mail.subject, msg.subject);
t.is(mail.from?.text, msg.from);
@ -390,69 +376,80 @@ test('streams message', async (t) => {
});
test('message validation fails without `from` header', async (t) => {
const { message: error } = await t.throwsAsync(validate({}));
t.is(error, 'Message must have a `from` header');
const msg = new Message({});
const { isValid, validationError } = msg.checkValidity();
t.false(isValid);
t.is(validationError, 'Message must have a `from` header');
});
test('message validation fails without `to`, `cc`, or `bcc` header', async (t) => {
const { message: error } = await t.throwsAsync(
validate({
from: 'piglet@gmail.com',
})
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
}).checkValidity();
t.false(isValid);
t.is(
validationError,
'Message must have at least one `to`, `cc`, or `bcc` header'
);
t.is(error, 'Message must have at least one `to`, `cc`, or `bcc` header');
});
test('message validation succeeds with only `to` recipient header (string)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
to: 'pooh@gmail.com',
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});
test('message validation succeeds with only `to` recipient header (array)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
to: ['pooh@gmail.com'],
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});
test('message validation succeeds with only `cc` recipient header (string)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
cc: 'pooh@gmail.com',
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});
test('message validation succeeds with only `cc` recipient header (array)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
cc: ['pooh@gmail.com'],
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});
test('message validation succeeds with only `bcc` recipient header (string)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
bcc: 'pooh@gmail.com',
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});
test('message validation succeeds with only `bcc` recipient header (array)', async (t) => {
const isValid = validate({
const { isValid, validationError } = new Message({
from: 'piglet@gmail.com',
bcc: ['pooh@gmail.com'],
});
await t.notThrowsAsync(isValid);
t.true(await isValid);
}).checkValidity();
t.true(isValid);
t.is(validationError, undefined);
});

View File

@ -1,6 +1,6 @@
// adapted from https://github.com/emailjs/emailjs-mime-codec/blob/6909c706b9f09bc0e5c3faf48f723cca53e5b352/src/mimecodec-unit.js
import test from 'ava';
import { mimeEncode, mimeWordEncode } from '../email';
import { mimeEncode, mimeWordEncode } from '../email.js';
test('mimeEncode should encode UTF-8', async (t) => {
t.is(mimeEncode('tere ÕÄÖÕ'), 'tere =C3=95=C3=84=C3=96=C3=95');

View File

@ -1,14 +1,11 @@
{
"extends": "@ledge/configs/tsconfig.json",
"include": [
"*.ts",
"smtp/**/*.ts",
"test/*.ts",
"email.ts",
"smtp",
"test",
],
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
}
"transpileOnly": true
}
}

2847
yarn.lock

File diff suppressed because it is too large Load Diff