mirror of https://github.com/eleith/emailjs.git
Compare commits
112 Commits
Author | SHA1 | Date |
---|---|---|
eleith | fc175932f6 | |
eleith | e11fac696b | |
eleith | 3a4ba01e01 | |
eleith | 426d270068 | |
eleith | 03694001e5 | |
eleith | ed54a25008 | |
eleith | 70b89d9361 | |
eleith | a6063e44a6 | |
eleith | 8b3c0b16f8 | |
Zack Schuster | 7d772326d9 | |
Zack Schuster | a395f862ec | |
Zack Schuster | 4f45799a3e | |
Zack Schuster | 9a5ce38186 | |
Zack Schuster | a7b468908f | |
Zack Schuster | 793fa7466c | |
eleith | bdf156971e | |
eleith | 8c67cf0160 | |
eleith | 6dc55e013e | |
Zack Schuster | c0f7a171ea | |
Zack Schuster | 2fc0e8c493 | |
Zack Schuster | 851b345d33 | |
Zack Schuster | f242b96dae | |
Zack Schuster | 79a81538aa | |
Zack Schuster | 652684486c | |
Zack Schuster | 5ee7d3b3b8 | |
Zack Schuster | f0cd1ce544 | |
eleith | b91cf6c97f | |
Zack Schuster | 24c313669c | |
Zack Schuster | 1d905b0d66 | |
Zack Schuster | f064ff1302 | |
Jeremy Möglich | 9d5b5376db | |
Zack Schuster | 54e03335bf | |
Zack Schuster | 6ae32a4c8e | |
Zack Schuster | f9b84cf0fd | |
Zack Schuster | c1d0aee0b1 | |
eleith | 607de0f6a6 | |
eleith | a965d9bc42 | |
Zack Schuster | e38e1b426f | |
Zack Schuster | 8496793ed6 | |
Zack Schuster | 495d8fc838 | |
Zack Schuster | d184a82bfe | |
Zack Schuster | 6d48c82aaf | |
Zack Schuster | 9c0735ea89 | |
Zack Schuster | 87b86299a0 | |
Zack Schuster | 9f7ddf0c6b | |
Zack Schuster | 89c0d574b0 | |
Zack Schuster | bceb38d8aa | |
Zack Schuster | e801eb54c9 | |
Zack Schuster | 1fe1557ca9 | |
Zack Schuster | 98346475e2 | |
Zack Schuster | 0480014ed4 | |
eleith | 99cf10fea8 | |
eleith | 6ee37bfeb7 | |
Zack Schuster | ffd17bca34 | |
Zack Schuster | d1424fb49f | |
Zack Schuster | 52b82711f1 | |
Zack Schuster | 0d497e9072 | |
Zack Schuster | c31412f0ea | |
Zack Schuster | 82c5bffac4 | |
Zack Schuster | 4374122320 | |
Zack Schuster | a2b240d177 | |
Zack Schuster | 81cc94b929 | |
Zack Schuster | 6f435a402e | |
Carson Full | 0516e3825c | |
Zack Schuster | 63401a0868 | |
Zack Schuster | d7b9236ed0 | |
Carson Full | 502196e350 | |
Carson Full | 177a03595b | |
Zack Schuster | aa35ea6b86 | |
Zack Schuster | 416b3e59c6 | |
eleith | 30aef9ab8c | |
eleith | 78d83bfb88 | |
Zack Schuster | e2adced6e4 | |
Zack Schuster | 306c5e9acc | |
Zack Schuster | 2600a79d93 | |
Zack Schuster | aa0add8b9d | |
Herr Ritschwumm | 512dbac584 | |
Herr Ritschwumm | 1d488a49ab | |
Zack Schuster | a6973c31ec | |
Zack Schuster | f46b85ce3a | |
Zack Schuster | 723b68ed19 | |
Zack Schuster | 99bdf2fb14 | |
Zack Schuster | 3403819397 | |
eleith | ad2e355286 | |
eleith | bc1fcf1f8e | |
Zack Schuster | c1c4d0b81b | |
Zack Schuster | afe2105919 | |
Zack Schuster | f8a691f80a | |
Zack Schuster | 27d51c2300 | |
Zack Schuster | f56b3c106b | |
Zack Schuster | ca135f8f38 | |
Zack Schuster | 44d31b579d | |
Zack Schuster | 987bc95e7f | |
Zack Schuster | 9cd062f10d | |
Zack Schuster | d799559261 | |
Zack Schuster | c13356b51e | |
Zack Schuster | b6aa8133b1 | |
Zack Schuster | 21277baf29 | |
Zack Schuster | 3253f3341b | |
Zack Schuster | eac98d2611 | |
Zack Schuster | 328cd92258 | |
Zack Schuster | 3bc1ad626e | |
Zack Schuster | 549ecdd9cb | |
Zack Schuster | b7ddab600b | |
Zack Schuster | d4930cd2da | |
Zack Schuster | c393115df6 | |
Zack Schuster | df3481637c | |
Zack Schuster | 1a78a74306 | |
Zack Schuster | 0f66e9cf91 | |
Zack Schuster | 3e85f2fbba | |
Zack Schuster | ccf17f97a1 | |
Zack Schuster | c51ccb0826 |
|
@ -1,15 +1,5 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"ecmaFeatures": {
|
||||
"modules": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
|
@ -19,9 +9,6 @@
|
|||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
|
@ -36,16 +23,6 @@
|
|||
"error",
|
||||
"unix"
|
||||
],
|
||||
"no-case-declarations": "off",
|
||||
"no-console": "off",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"valid-jsdoc": "error"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*.ts text eol=lf
|
|
@ -0,0 +1,26 @@
|
|||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [^12, ^14, ^16, ^18]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: install
|
||||
run: yarn install
|
||||
|
||||
- name: lint
|
||||
run: yarn lint
|
|
@ -7,25 +7,20 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [^10, ^12, ^14]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
node: [^12, ^14, ^16, ^18]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: install
|
||||
run: yarn install
|
||||
|
||||
- name: test
|
||||
run: yarn test
|
||||
|
||||
- name: test-cjs
|
||||
run: yarn test-cjs
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
*~
|
||||
*.apache
|
||||
*.apache2
|
||||
*.bnf
|
||||
*.browser.*
|
||||
*.bsd
|
||||
*.coffee
|
||||
*.conf
|
||||
*.conf.*
|
||||
*.config.*
|
||||
*.css
|
||||
*.docs
|
||||
*.flow*
|
||||
*.gif
|
||||
*.gz
|
||||
*.html
|
||||
*.ico
|
||||
*.iml
|
||||
*.ini
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.lcov
|
||||
*.lock
|
||||
*.log
|
||||
*.ls
|
||||
*.map
|
||||
*.markdown
|
||||
*.md
|
||||
*.mit
|
||||
*.patch
|
||||
*.png
|
||||
*.sh
|
||||
*.swf
|
||||
*.txt
|
||||
*.yml
|
||||
*-browser.*
|
||||
*_browser.*
|
||||
*_example.*
|
||||
*install*
|
||||
*publish*
|
||||
.*config
|
||||
.*ignore
|
||||
.*rc
|
||||
.*rc.*
|
||||
.git*
|
||||
.*code
|
||||
_ts3.4
|
||||
authors
|
||||
bench
|
||||
benchmark
|
||||
benchmarks
|
||||
bower.json
|
||||
browser.*
|
||||
changelog
|
||||
component.json
|
||||
demo
|
||||
docs
|
||||
example
|
||||
examples
|
||||
fixtures
|
||||
fp
|
||||
gruntfile.*
|
||||
gulpfile.*
|
||||
jsdoc*
|
||||
licence*
|
||||
license*
|
||||
makefile
|
||||
man
|
||||
scripts
|
||||
spec
|
||||
test
|
||||
test-*
|
||||
!test-*.d.ts
|
||||
tests
|
||||
tsserverlibrary.*
|
||||
typescriptservices.*
|
||||
webpack*
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -4,6 +4,56 @@ 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)
|
||||
- `isRFC2822Date` API
|
||||
|
||||
### Changed
|
||||
- use `WeakSet` instead of `WeakMap` for greylist tracking
|
||||
|
||||
### Fixed
|
||||
- use camelCase style for internal function names
|
||||
- use correct types in jsdoc comments
|
||||
|
||||
## [3.3.0] - 2020-08-08
|
||||
### Added
|
||||
- greylist support [#202](https://github.com/eleith/emailjs/issues/202)
|
||||
|
|
44
README.md
44
README.md
|
@ -1,4 +1,4 @@
|
|||
# emailjs [![Test Status](https://github.com/eleith/emailjs/workflows/.github/workflows/test.yml/badge.svg)](https://github.com/eleith/emailjs/actions?query=workflow%3A.github%2Fworkflows%2Ftest.yml)
|
||||
# emailjs [![Test Status](https://github.com/eleith/emailjs/workflows/.github/workflows/test.yml/badge.svg)](https://github.com/eleith/emailjs/actions?query=workflow%3A.github%2Fworkflows%2Ftest.yml) [![Lint Status](https://github.com/eleith/emailjs/workflows/.github/workflows/lint.yml/badge.svg)](https://github.com/eleith/emailjs/actions?query=workflow%3A.github%2Fworkflows%2Flint.yml)
|
||||
|
||||
send emails, html and attachments (files, streams and strings) from node.js to any smtp server
|
||||
|
||||
|
@ -49,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
|
||||
|
@ -235,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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
@ -252,6 +252,19 @@ function getRFC2822DateUTC(date = new Date()) {
|
|||
dates.push('+0000');
|
||||
return dates.join(' ');
|
||||
}
|
||||
/**
|
||||
* RFC 2822 regex
|
||||
* @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}))$/;
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
function isRFC2822Date(date) {
|
||||
return rfc2822re.test(date);
|
||||
}
|
||||
|
||||
// adapted from https://github.com/emailjs/emailjs-mime-codec/blob/6909c706b9f09bc0e5c3faf48f723cca53e5b352/src/mimecodec.js
|
||||
const encoder = new TextEncoder();
|
||||
|
@ -263,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
|
||||
|
@ -440,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.
|
||||
*/
|
||||
|
@ -454,7 +467,7 @@ const MIME64CHUNK = (MIMECHUNK * 6);
|
|||
*/
|
||||
const BUFFERSIZE = (MIMECHUNK * 24 * 7);
|
||||
let counter = 0;
|
||||
function generate_boundary() {
|
||||
function generateBoundary() {
|
||||
let text = '';
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
|
||||
for (let i = 0; i < 69; i++) {
|
||||
|
@ -552,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`);
|
||||
}
|
||||
}
|
||||
|
@ -588,12 +603,26 @@ 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
|
||||
* @returns {*} a stream of the current message
|
||||
* @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
|
||||
* @returns {MessageStream} a stream of the current message
|
||||
*/
|
||||
stream() {
|
||||
return new MessageStream(this);
|
||||
|
@ -610,10 +639,22 @@ 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 {
|
||||
/**
|
||||
* @param {*} message the message to stream
|
||||
* @param {Message} message the message to stream
|
||||
*/
|
||||
constructor(message) {
|
||||
super();
|
||||
|
@ -666,7 +707,7 @@ class MessageStream extends Stream {
|
|||
* @param {MessageAttachment} [attachment] the attachment whose headers you would like to output
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_attachment_headers = (attachment) => {
|
||||
const outputAttachmentHeaders = (attachment) => {
|
||||
let data = [];
|
||||
const headers = {
|
||||
'content-type': attachment.type +
|
||||
|
@ -688,89 +729,81 @@ 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
|
||||
* @param {function(): void} [callback] the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_base64 = (data, callback) => {
|
||||
const outputBase64 = (data, callback) => {
|
||||
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) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const output_file = (attachment, next) => {
|
||||
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
|
||||
output_base64(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
|
||||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_stream = (attachment, callback) => {
|
||||
const outputStream = (attachment, callback) => {
|
||||
const { stream } = attachment;
|
||||
if (stream === null || stream === void 0 ? void 0 : stream.readable) {
|
||||
let previous = Buffer.alloc(0);
|
||||
stream.resume();
|
||||
stream.on('end', () => {
|
||||
output_base64(previous.toString('base64'), callback);
|
||||
outputBase64(previous.toString('base64'), callback);
|
||||
this.removeListener('pause', stream.pause);
|
||||
this.removeListener('resume', stream.resume);
|
||||
this.removeListener('error', stream.resume);
|
||||
|
@ -788,7 +821,7 @@ class MessageStream extends Stream {
|
|||
// copy dangling bytes into previous buffer
|
||||
buffer.copy(previous, 0, buffer.length - padded);
|
||||
}
|
||||
output_base64(buffer.toString('base64', 0, buffer.length - padded));
|
||||
outputBase64(buffer.toString('base64', 0, buffer.length - padded));
|
||||
});
|
||||
this.on('pause', stream.pause);
|
||||
this.on('resume', stream.resume);
|
||||
|
@ -798,13 +831,13 @@ class MessageStream extends Stream {
|
|||
this.emit('error', { message: 'stream not readable' });
|
||||
}
|
||||
};
|
||||
const output_attachment = (attachment, callback) => {
|
||||
const outputAttachment = (attachment, callback) => {
|
||||
const build = attachment.path
|
||||
? output_file
|
||||
? outputFile
|
||||
: attachment.stream
|
||||
? output_stream
|
||||
: output_data;
|
||||
output_attachment_headers(attachment);
|
||||
? outputStream
|
||||
: outputData;
|
||||
outputAttachmentHeaders(attachment);
|
||||
build(attachment, callback);
|
||||
};
|
||||
/**
|
||||
|
@ -814,32 +847,32 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call if index is greater than upper bound
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_message = (boundary, list, index, callback) => {
|
||||
const outputMessage = (boundary, list, index, callback) => {
|
||||
if (index < list.length) {
|
||||
output(`--${boundary}${CRLF}`);
|
||||
output(`--${boundary}${CRLF$1}`);
|
||||
if (list[index].related) {
|
||||
output_related(list[index], () => output_message(boundary, list, index + 1, callback));
|
||||
outputRelated(list[index], () => outputMessage(boundary, list, index + 1, callback));
|
||||
}
|
||||
else {
|
||||
output_attachment(list[index], () => output_message(boundary, list, index + 1, callback));
|
||||
outputAttachment(list[index], () => outputMessage(boundary, list, index + 1, callback));
|
||||
}
|
||||
}
|
||||
else {
|
||||
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
||||
output(`${CRLF$1}--${boundary}--${CRLF$1}${CRLF$1}`);
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const output_mixed = () => {
|
||||
const boundary = generate_boundary();
|
||||
output(`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
|
||||
const outputMixed = () => {
|
||||
const boundary = generateBoundary();
|
||||
output(`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
|
||||
if (this.message.alternative == null) {
|
||||
output_text(this.message);
|
||||
output_message(boundary, this.message.attachments, 0, close);
|
||||
outputText(this.message);
|
||||
outputMessage(boundary, this.message.attachments, 0, close$1);
|
||||
}
|
||||
else {
|
||||
output_alternative(
|
||||
outputAlternative(
|
||||
// typescript bug; should narrow to { alternative: MessageAttachment }
|
||||
this.message, () => output_message(boundary, this.message.attachments, 0, close));
|
||||
this.message, () => outputMessage(boundary, this.message.attachments, 0, close$1));
|
||||
}
|
||||
};
|
||||
/**
|
||||
|
@ -847,26 +880,27 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_data = (attachment, callback) => {
|
||||
const outputData = (attachment, callback) => {
|
||||
var _a, _b;
|
||||
output_base64(attachment.encoded
|
||||
? (_a = attachment.data) !== null && _a !== void 0 ? _a : '' : Buffer.from((_b = attachment.data) !== null && _b !== void 0 ? _b : '').toString('base64'), callback);
|
||||
outputBase64(attachment.encoded
|
||||
? (_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
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_text = (message) => {
|
||||
const outputText = (message) => {
|
||||
let data = [];
|
||||
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(''));
|
||||
};
|
||||
/**
|
||||
|
@ -874,13 +908,13 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_related = (message, callback) => {
|
||||
const boundary = generate_boundary();
|
||||
output(`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
|
||||
output_attachment(message, () => {
|
||||
const outputRelated = (message, callback) => {
|
||||
const boundary = generateBoundary();
|
||||
output(`Content-Type: multipart/related; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
|
||||
outputAttachment(message, () => {
|
||||
var _a;
|
||||
output_message(boundary, (_a = message.related) !== null && _a !== void 0 ? _a : [], 0, () => {
|
||||
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
||||
outputMessage(boundary, (_a = message.related) !== null && _a !== void 0 ? _a : [], 0, () => {
|
||||
output(`${CRLF$1}--${boundary}--${CRLF$1}${CRLF$1}`);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
@ -890,26 +924,26 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_alternative = (message, callback) => {
|
||||
const boundary = generate_boundary();
|
||||
output(`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`);
|
||||
output_text(message);
|
||||
output(`--${boundary}${CRLF}`);
|
||||
const outputAlternative = (message, callback) => {
|
||||
const boundary = generateBoundary();
|
||||
output(`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF$1}${CRLF$1}--${boundary}${CRLF$1}`);
|
||||
outputText(message);
|
||||
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) {
|
||||
output_related(message.alternative, finish);
|
||||
outputRelated(message.alternative, finish);
|
||||
}
|
||||
else {
|
||||
output_attachment(message.alternative, finish);
|
||||
outputAttachment(message.alternative, finish);
|
||||
}
|
||||
};
|
||||
const close = (err) => {
|
||||
const close$1 = (err) => {
|
||||
var _a, _b;
|
||||
if (err) {
|
||||
this.emit('error', err);
|
||||
|
@ -930,20 +964,20 @@ class MessageStream extends Stream {
|
|||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_header_data = () => {
|
||||
const outputHeaderData = () => {
|
||||
if (this.message.attachments.length || this.message.alternative) {
|
||||
output(`MIME-Version: 1.0${CRLF}`);
|
||||
output_mixed();
|
||||
output(`MIME-Version: 1.0${CRLF$1}`);
|
||||
outputMixed();
|
||||
} // you only have a text message!
|
||||
else {
|
||||
output_text(this.message);
|
||||
close();
|
||||
outputText(this.message);
|
||||
close$1();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_header = () => {
|
||||
const outputHeader = () => {
|
||||
let data = [];
|
||||
for (const header in this.message.header) {
|
||||
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
|
||||
|
@ -953,15 +987,15 @@ class MessageStream extends Stream {
|
|||
convertDashDelimitedTextToSnakeCase(header),
|
||||
': ',
|
||||
this.message.header[header],
|
||||
CRLF,
|
||||
CRLF$1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
output(data.join(''));
|
||||
output_header_data();
|
||||
outputHeaderData();
|
||||
};
|
||||
this.once('destroy', close);
|
||||
process.nextTick(output_header);
|
||||
this.once('destroy', close$1);
|
||||
process.nextTick(outputHeader);
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
|
@ -1128,11 +1162,11 @@ 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;
|
||||
/**
|
||||
* @param {...any} args the message(s) to log
|
||||
* @param {...any[]} args the message(s) to log
|
||||
* @returns {void}
|
||||
*/
|
||||
const log = (...args) => {
|
||||
|
@ -1145,8 +1179,8 @@ const log = (...args) => {
|
|||
}
|
||||
};
|
||||
/**
|
||||
* @param {function(...*): void} callback the function to call
|
||||
* @param {...*} args the arguments to apply to the function
|
||||
* @param {function(...any[]): void} callback the function to call
|
||||
* @param {...any[]} args the arguments to apply to the function
|
||||
* @returns {void}
|
||||
*/
|
||||
const caller = (callback, ...args) => {
|
||||
|
@ -1183,7 +1217,7 @@ class SMTPConnection extends EventEmitter {
|
|||
this.host = 'localhost';
|
||||
this.ssl = false;
|
||||
this.tls = false;
|
||||
this.greylistResponseTracker = new WeakMap();
|
||||
this.greylistResponseTracker = new WeakSet();
|
||||
if (Array.isArray(authentication)) {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
@ -1215,7 +1249,7 @@ class SMTPConnection extends EventEmitter {
|
|||
this.user = () => user;
|
||||
this.password = () => password;
|
||||
if (typeof logger === 'function') {
|
||||
this.log = log;
|
||||
this.log = logger;
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -1246,7 +1280,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* NOTE: `host` is trimmed before being used to establish a connection; however, the original untrimmed value will still be visible in configuration.
|
||||
*
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {number} [port] the port to use for the connection
|
||||
* @param {string} [host] the hostname to use for the connection
|
||||
* @param {ConnectOptions} [options={}] the options
|
||||
|
@ -1328,7 +1362,7 @@ class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @public
|
||||
* @param {string} str the string to send
|
||||
* @param {*} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
send(str, callback) {
|
||||
|
@ -1355,7 +1389,7 @@ class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @public
|
||||
* @param {string} cmd command to issue
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {(number[] | number)} [codes=[250]] array codes
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1376,10 +1410,10 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
else if ((code === 450 || code === 451) &&
|
||||
msg.message.toLowerCase().includes('greylist') &&
|
||||
this.greylistResponseTracker.get(response) === false) {
|
||||
this.greylistResponseTracker.set(response, true);
|
||||
this.greylistResponseTracker.has(response) === false) {
|
||||
this.greylistResponseTracker.add(response);
|
||||
setTimeout(() => {
|
||||
this.send(cmd + CRLF$1, response);
|
||||
this.send(cmd + CRLF, response);
|
||||
}, GREYLIST_DELAY);
|
||||
}
|
||||
else {
|
||||
|
@ -1389,8 +1423,8 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
}
|
||||
};
|
||||
this.greylistResponseTracker.set(response, false);
|
||||
this.send(cmd + CRLF$1, response);
|
||||
this.greylistResponseTracker.delete(response);
|
||||
this.send(cmd + CRLF, response);
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
|
@ -1402,7 +1436,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* As this command was deprecated by rfc2821, it should only be used for compatibility with non-compliant servers.
|
||||
* @see https://tools.ietf.org/html/rfc2821#appendix-F.3
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'helo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1419,7 +1453,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
starttls(callback) {
|
||||
|
@ -1474,7 +1508,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'ehlo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1507,7 +1541,7 @@ class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @public
|
||||
* @description SMTP 'help' command, returns text from the server
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'help' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1516,7 +1550,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
rset(callback) {
|
||||
|
@ -1524,7 +1558,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
noop(callback) {
|
||||
|
@ -1532,7 +1566,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} from the sender
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1541,7 +1575,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} to the receiver
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1550,7 +1584,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
data(callback) {
|
||||
|
@ -1558,11 +1592,11 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
data_end(callback) {
|
||||
this.command(`${CRLF$1}.`, callback);
|
||||
this.command(`${CRLF}.`, callback);
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
|
@ -1578,7 +1612,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* @public
|
||||
* @description SMTP 'verify' command -- checks for address validity.
|
||||
* @param {string} address the address to validate
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
verify(address, callback) {
|
||||
|
@ -1588,7 +1622,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* @public
|
||||
* @description SMTP 'expn' command -- expands a mailing list.
|
||||
* @param {string} address the mailing list to expand
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
expn(address, callback) {
|
||||
|
@ -1601,7 +1635,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* If there has been no previous EHLO or HELO command self session, self
|
||||
* method tries ESMTP EHLO first.
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} [domain] the domain to associate with the command
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1629,7 +1663,7 @@ class SMTPConnection extends EventEmitter {
|
|||
*
|
||||
* This method will return normally if the authentication was successful.
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} [user] the username to authenticate with
|
||||
* @param {string} [password] the password for the authentication
|
||||
* @param {{ method: string, domain: string }} [options] login options
|
||||
|
@ -1654,7 +1688,7 @@ class SMTPConnection extends EventEmitter {
|
|||
* @param {string} challenge challenge
|
||||
* @returns {string} base64 cram hash
|
||||
*/
|
||||
const encode_cram_md5 = (challenge) => {
|
||||
const encodeCramMd5 = (challenge) => {
|
||||
const hmac = createHmac('md5', login.password());
|
||||
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
|
||||
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString('base64');
|
||||
|
@ -1662,19 +1696,19 @@ class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @returns {string} base64 login/password
|
||||
*/
|
||||
const encode_plain = () => Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString('base64');
|
||||
const encodePlain = () => Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString('base64');
|
||||
/**
|
||||
* @see https://developers.google.com/gmail/xoauth2_protocol
|
||||
* @returns {string} base64 xoauth2 auth token
|
||||
*/
|
||||
const encode_xoauth2 = () => Buffer.from(`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`).toString('base64');
|
||||
const encodeXoauth2 = () => Buffer.from(`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`).toString('base64');
|
||||
// List of authentication methods we support: from preferred to
|
||||
// less preferred methods.
|
||||
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])) {
|
||||
|
@ -1686,17 +1720,18 @@ class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* handle bad responses from command differently
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @returns {void}
|
||||
*/
|
||||
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));
|
||||
};
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @returns {void}
|
||||
*/
|
||||
const response = (err, data) => {
|
||||
|
@ -1710,7 +1745,7 @@ class SMTPConnection extends EventEmitter {
|
|||
};
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -1720,7 +1755,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
else {
|
||||
if (method === AUTH_METHODS['CRAM-MD5']) {
|
||||
this.command(encode_cram_md5(msg), response, [235, 503]);
|
||||
this.command(encodeCramMd5(msg), response, [235, 503]);
|
||||
}
|
||||
else if (method === AUTH_METHODS.LOGIN) {
|
||||
this.command(Buffer.from(login.password()).toString('base64'), response, [235, 503]);
|
||||
|
@ -1729,11 +1764,11 @@ class SMTPConnection extends EventEmitter {
|
|||
};
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
const attempt_user = (err, data) => {
|
||||
const attemptUser = (err, data) => {
|
||||
if (err) {
|
||||
failed(err, data);
|
||||
}
|
||||
|
@ -1748,18 +1783,16 @@ class SMTPConnection extends EventEmitter {
|
|||
this.command(`AUTH ${AUTH_METHODS['CRAM-MD5']}`, attempt, [334]);
|
||||
break;
|
||||
case AUTH_METHODS.LOGIN:
|
||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
|
||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attemptUser, [334]);
|
||||
break;
|
||||
case AUTH_METHODS.PLAIN:
|
||||
this.command(`AUTH ${AUTH_METHODS.PLAIN} ${encode_plain()}`, response, [235, 503]);
|
||||
this.command(`AUTH ${AUTH_METHODS.PLAIN} ${encodePlain()}`, response, [235, 503]);
|
||||
break;
|
||||
case AUTH_METHODS.XOAUTH2:
|
||||
this.command(`AUTH ${AUTH_METHODS.XOAUTH2} ${encode_xoauth2()}`, response, [235, 503]);
|
||||
this.command(`AUTH ${AUTH_METHODS.XOAUTH2} ${encodeXoauth2()}`, response, [235, 503]);
|
||||
break;
|
||||
default:
|
||||
const msg = 'no form of authorization supported';
|
||||
const err = SMTPError.create(msg, SMTPErrorStates.AUTHNOTSUPPORTED, null, data);
|
||||
caller(callback, err);
|
||||
caller(callback, SMTPError.create('no form of authorization supported', SMTPErrorStates.AUTHNOTSUPPORTED, null, data));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1793,7 +1826,7 @@ class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} [callback] function to call after response
|
||||
* @param {function(...any[]): void} [callback] function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
quit(callback) {
|
||||
|
@ -1821,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) {
|
||||
|
@ -1835,25 +1869,44 @@ 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);
|
||||
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
|
||||
* @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, message) => {
|
||||
if (err != null) {
|
||||
reject(err);
|
||||
}
|
||||
this.queue.push(stack);
|
||||
this._poll();
|
||||
}
|
||||
else {
|
||||
callback(new Error(why), msg);
|
||||
}
|
||||
else {
|
||||
// unfortunately, the conditional type doesn't reach here
|
||||
// fortunately, we only return a `Message` when err is null, so this is safe
|
||||
resolve(message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @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 () {
|
||||
|
@ -1962,8 +2015,8 @@ class SMTPClient {
|
|||
}
|
||||
/**
|
||||
* @protected
|
||||
* @param {*} attachment attachment
|
||||
* @returns {*} whether the attachment contains inlined html
|
||||
* @param {MessageAttachment | MessageAttachment[]} attachment attachment
|
||||
* @returns {boolean} whether the attachment contains inlined html
|
||||
*/
|
||||
_containsInlinedHtml(attachment) {
|
||||
if (Array.isArray(attachment)) {
|
||||
|
@ -2069,5 +2122,5 @@ class SMTPClient {
|
|||
}
|
||||
}
|
||||
|
||||
export { AUTH_METHODS, BUFFERSIZE, DEFAULT_TIMEOUT, MIME64CHUNK, MIMECHUNK, Message, SMTPClient, SMTPConnection, SMTPError, SMTPErrorStates, SMTPResponseMonitor, SMTPState, addressparser, getRFC2822Date, getRFC2822DateUTC, mimeEncode, mimeWordEncode };
|
||||
//# sourceMappingURL=email.mjs.map
|
||||
export { AUTH_METHODS, BUFFERSIZE, DEFAULT_TIMEOUT, MIME64CHUNK, MIMECHUNK, Message, SMTPClient, SMTPConnection, SMTPError, SMTPErrorStates, SMTPResponseMonitor, SMTPState, addressparser, getRFC2822Date, getRFC2822DateUTC, isRFC2822Date, mimeEncode, mimeWordEncode };
|
||||
//# sourceMappingURL=email.js.map
|
File diff suppressed because one or more lines are too long
|
@ -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');
|
||||
}
|
16
email.ts
16
email.ts
|
@ -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';
|
||||
|
|
67
package.json
67
package.json
|
@ -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.3.0",
|
||||
"version": "4.0.3",
|
||||
"author": "eleith",
|
||||
"contributors": [
|
||||
"izuzak",
|
||||
|
@ -16,44 +16,53 @@
|
|||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@ledge/configs": "23.0.0",
|
||||
"@rollup/plugin-typescript": "5.0.2",
|
||||
"@types/mailparser": "2.7.3",
|
||||
"@types/smtp-server": "3.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "3.7.1",
|
||||
"@typescript-eslint/parser": "3.7.1",
|
||||
"ava": "3.11.0",
|
||||
"eslint": "7.5.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-prettier": "3.1.4",
|
||||
"mailparser": "2.8.0",
|
||||
"prettier": "2.0.5",
|
||||
"rollup": "2.23.0",
|
||||
"smtp-server": "3.7.0",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "3.9.7"
|
||||
"@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"
|
||||
}
|
||||
|
|
|
@ -1,26 +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: false,
|
||||
freeze: false,
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: 'rollup/email.mjs',
|
||||
format: 'es',
|
||||
interop: false,
|
||||
freeze: false,
|
||||
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/*'] }),
|
||||
],
|
||||
};
|
||||
|
|
2093
rollup/email.cjs
2093
rollup/email.cjs
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
|
@ -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,17 +66,37 @@ 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
|
||||
* @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<T extends Message | MessageHeaders>(msg: T) {
|
||||
return new Promise<Message>((resolve, reject) => {
|
||||
this.send(msg, (err, message) => {
|
||||
if (err != null) {
|
||||
reject(err);
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -73,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 () {
|
||||
/* ø */
|
||||
}
|
||||
) {
|
||||
|
@ -210,11 +241,11 @@ export class SMTPClient {
|
|||
|
||||
/**
|
||||
* @protected
|
||||
* @param {*} attachment attachment
|
||||
* @returns {*} whether the attachment contains inlined html
|
||||
* @param {MessageAttachment | MessageAttachment[]} attachment attachment
|
||||
* @returns {boolean} whether the attachment contains inlined html
|
||||
*/
|
||||
protected _containsInlinedHtml(
|
||||
attachment: MessageAttachment | MessageAttachment[]
|
||||
attachment?: MessageAttachment | MessageAttachment[]
|
||||
) {
|
||||
if (Array.isArray(attachment)) {
|
||||
return attachment.some((att) => {
|
||||
|
@ -230,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) &&
|
||||
|
|
|
@ -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
|
||||
|
@ -44,7 +40,7 @@ const GREYLIST_DELAY = 300 as const;
|
|||
let DEBUG: 0 | 1 = 0;
|
||||
|
||||
/**
|
||||
* @param {...any} args the message(s) to log
|
||||
* @param {...any[]} args the message(s) to log
|
||||
* @returns {void}
|
||||
*/
|
||||
const log = (...args: any[]) => {
|
||||
|
@ -62,8 +58,8 @@ const log = (...args: any[]) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @param {function(...*): void} callback the function to call
|
||||
* @param {...*} args the arguments to apply to the function
|
||||
* @param {function(...any[]): void} callback the function to call
|
||||
* @param {...any[]} args the arguments to apply to the function
|
||||
* @returns {void}
|
||||
*/
|
||||
const caller = (callback?: (...rest: any[]) => void, ...args: any[]) => {
|
||||
|
@ -120,10 +116,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
protected tls: boolean | SMTPSocketOptions = false;
|
||||
protected port: number;
|
||||
|
||||
private greylistResponseTracker = new WeakMap<
|
||||
(...rest: any[]) => void,
|
||||
boolean
|
||||
>();
|
||||
private greylistResponseTracker = new WeakSet<(...rest: any[]) => void>();
|
||||
|
||||
/**
|
||||
* SMTP class written using python's (2.7) smtplib.py as a base.
|
||||
|
@ -190,7 +183,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
this.password = () => password as string;
|
||||
|
||||
if (typeof logger === 'function') {
|
||||
this.log = log;
|
||||
this.log = logger;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,7 +218,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* NOTE: `host` is trimmed before being used to establish a connection; however, the original untrimmed value will still be visible in configuration.
|
||||
*
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {number} [port] the port to use for the connection
|
||||
* @param {string} [host] the hostname to use for the connection
|
||||
* @param {ConnectOptions} [options={}] the options
|
||||
|
@ -350,7 +343,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @public
|
||||
* @param {string} str the string to send
|
||||
* @param {*} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public send(str: string, callback: (...args: any[]) => void) {
|
||||
|
@ -383,7 +376,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @public
|
||||
* @param {string} cmd command to issue
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {(number[] | number)} [codes=[250]] array codes
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -411,9 +404,9 @@ export class SMTPConnection extends EventEmitter {
|
|||
} else if (
|
||||
(code === 450 || code === 451) &&
|
||||
msg.message.toLowerCase().includes('greylist') &&
|
||||
this.greylistResponseTracker.get(response) === false
|
||||
this.greylistResponseTracker.has(response) === false
|
||||
) {
|
||||
this.greylistResponseTracker.set(response, true);
|
||||
this.greylistResponseTracker.add(response);
|
||||
setTimeout(() => {
|
||||
this.send(cmd + CRLF, response);
|
||||
}, GREYLIST_DELAY);
|
||||
|
@ -435,7 +428,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
}
|
||||
};
|
||||
|
||||
this.greylistResponseTracker.set(response, false);
|
||||
this.greylistResponseTracker.delete(response);
|
||||
this.send(cmd + CRLF, response);
|
||||
}
|
||||
|
||||
|
@ -449,7 +442,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* As this command was deprecated by rfc2821, it should only be used for compatibility with non-compliant servers.
|
||||
* @see https://tools.ietf.org/html/rfc2821#appendix-F.3
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'helo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -466,7 +459,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public starttls(callback: (...rest: any[]) => void) {
|
||||
|
@ -534,7 +527,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'ehlo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -560,14 +553,14 @@ export class SMTPConnection extends EventEmitter {
|
|||
* @param {string} opt the features keyname to check
|
||||
* @returns {boolean} whether the extension exists
|
||||
*/
|
||||
public has_extn(opt: string): boolean {
|
||||
public has_extn(opt: string) {
|
||||
return (this.features ?? {})[opt.toLowerCase()] === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @description SMTP 'help' command, returns text from the server
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} domain the domain to associate with the 'help' request
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -577,7 +570,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public rset(callback: (...rest: any[]) => void) {
|
||||
|
@ -586,7 +579,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public noop(callback: (...rest: any[]) => void) {
|
||||
|
@ -595,7 +588,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} from the sender
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -605,7 +598,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} to the receiver
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -615,7 +608,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public data(callback: (...rest: any[]) => void) {
|
||||
|
@ -624,7 +617,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public data_end(callback: (...rest: any[]) => void) {
|
||||
|
@ -645,7 +638,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* @public
|
||||
* @description SMTP 'verify' command -- checks for address validity.
|
||||
* @param {string} address the address to validate
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public verify(address: string, callback: (...rest: any[]) => void) {
|
||||
|
@ -656,7 +649,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* @public
|
||||
* @description SMTP 'expn' command -- expands a mailing list.
|
||||
* @param {string} address the mailing list to expand
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public expn(address: string, callback: (...rest: any[]) => void) {
|
||||
|
@ -670,7 +663,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* If there has been no previous EHLO or HELO command self session, self
|
||||
* method tries ESMTP EHLO first.
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} [domain] the domain to associate with the command
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -702,7 +695,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
*
|
||||
* This method will return normally if the authentication was successful.
|
||||
*
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {function(...any[]): void} callback function to call after response
|
||||
* @param {string} [user] the username to authenticate with
|
||||
* @param {string} [password] the password for the authentication
|
||||
* @param {{ method: string, domain: string }} [options] login options
|
||||
|
@ -734,7 +727,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* @param {string} challenge challenge
|
||||
* @returns {string} base64 cram hash
|
||||
*/
|
||||
const encode_cram_md5 = (challenge: string): string => {
|
||||
const encodeCramMd5 = (challenge: string) => {
|
||||
const hmac = createHmac('md5', login.password());
|
||||
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
|
||||
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
|
||||
|
@ -745,7 +738,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* @returns {string} base64 login/password
|
||||
*/
|
||||
const encode_plain = (): string =>
|
||||
const encodePlain = () =>
|
||||
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
|
||||
'base64'
|
||||
);
|
||||
|
@ -754,7 +747,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
* @see https://developers.google.com/gmail/xoauth2_protocol
|
||||
* @returns {string} base64 xoauth2 auth token
|
||||
*/
|
||||
const encode_xoauth2 = (): string =>
|
||||
const encodeXoauth2 = () =>
|
||||
Buffer.from(
|
||||
`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`
|
||||
).toString('base64');
|
||||
|
@ -765,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++) {
|
||||
|
@ -780,12 +773,15 @@ export class SMTPConnection extends EventEmitter {
|
|||
/**
|
||||
* handle bad responses from command differently
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @returns {void}
|
||||
*/
|
||||
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(
|
||||
|
@ -799,7 +795,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @returns {void}
|
||||
*/
|
||||
const response = (err: Error | null | undefined, data: unknown) => {
|
||||
|
@ -813,7 +809,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -826,7 +822,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
failed(err, data);
|
||||
} else {
|
||||
if (method === AUTH_METHODS['CRAM-MD5']) {
|
||||
this.command(encode_cram_md5(msg), response, [235, 503]);
|
||||
this.command(encodeCramMd5(msg), response, [235, 503]);
|
||||
} else if (method === AUTH_METHODS.LOGIN) {
|
||||
this.command(
|
||||
Buffer.from(login.password()).toString('base64'),
|
||||
|
@ -839,11 +835,11 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @param {Error} err err
|
||||
* @param {*} data data
|
||||
* @param {unknown} data data
|
||||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
const attempt_user = (err: Error, data: unknown) => {
|
||||
const attemptUser = (err: Error, data: unknown) => {
|
||||
if (err) {
|
||||
failed(err, data);
|
||||
} else {
|
||||
|
@ -862,31 +858,32 @@ export class SMTPConnection extends EventEmitter {
|
|||
this.command(`AUTH ${AUTH_METHODS['CRAM-MD5']}`, attempt, [334]);
|
||||
break;
|
||||
case AUTH_METHODS.LOGIN:
|
||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
|
||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attemptUser, [334]);
|
||||
break;
|
||||
case AUTH_METHODS.PLAIN:
|
||||
this.command(
|
||||
`AUTH ${AUTH_METHODS.PLAIN} ${encode_plain()}`,
|
||||
`AUTH ${AUTH_METHODS.PLAIN} ${encodePlain()}`,
|
||||
response,
|
||||
[235, 503]
|
||||
);
|
||||
break;
|
||||
case AUTH_METHODS.XOAUTH2:
|
||||
this.command(
|
||||
`AUTH ${AUTH_METHODS.XOAUTH2} ${encode_xoauth2()}`,
|
||||
`AUTH ${AUTH_METHODS.XOAUTH2} ${encodeXoauth2()}`,
|
||||
response,
|
||||
[235, 503]
|
||||
);
|
||||
break;
|
||||
default:
|
||||
const msg = 'no form of authorization supported';
|
||||
const err = SMTPError.create(
|
||||
msg,
|
||||
SMTPErrorStates.AUTHNOTSUPPORTED,
|
||||
null,
|
||||
data
|
||||
caller(
|
||||
callback,
|
||||
SMTPError.create(
|
||||
'no form of authorization supported',
|
||||
SMTPErrorStates.AUTHNOTSUPPORTED,
|
||||
null,
|
||||
data
|
||||
)
|
||||
);
|
||||
caller(callback, err);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -924,7 +921,7 @@ export class SMTPConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} [callback] function to call after response
|
||||
* @param {function(...any[]): void} [callback] function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
public quit(callback?: (...rest: any[]) => void) {
|
||||
|
|
16
smtp/date.ts
16
smtp/date.ts
|
@ -33,3 +33,19 @@ export function getRFC2822DateUTC(date = new Date()) {
|
|||
dates.push('+0000');
|
||||
return dates.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 2822 regex
|
||||
* @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}))$/;
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
export function isRFC2822Date(date: string) {
|
||||
return rfc2822re.test(date);
|
||||
}
|
||||
|
|
257
smtp/message.ts
257
smtp/message.ts
|
@ -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,24 +68,25 @@ 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;
|
||||
|
||||
function generate_boundary() {
|
||||
function generateBoundary() {
|
||||
let text = '';
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
|
||||
|
@ -170,7 +178,7 @@ export class Message {
|
|||
* @param {MessageAttachment} options attachment options
|
||||
* @returns {Message} the current instance for chaining
|
||||
*/
|
||||
public attach(options: MessageAttachment): Message {
|
||||
public attach(options: MessageAttachment) {
|
||||
// sender can specify an attachment as an alternative
|
||||
if (options.alternative) {
|
||||
this.alternative = options;
|
||||
|
@ -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,14 +238,29 @@ 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
|
||||
* @returns {*} a stream of the current message
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @returns {MessageStream} a stream of the current message
|
||||
*/
|
||||
public stream() {
|
||||
return new MessageStream(this);
|
||||
|
@ -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 {
|
||||
|
@ -259,7 +299,7 @@ class MessageStream extends Stream {
|
|||
bufferIndex = 0;
|
||||
|
||||
/**
|
||||
* @param {*} message the message to stream
|
||||
* @param {Message} message the message to stream
|
||||
*/
|
||||
constructor(private message: Message) {
|
||||
super();
|
||||
|
@ -322,7 +362,7 @@ class MessageStream extends Stream {
|
|||
* @param {MessageAttachment} [attachment] the attachment whose headers you would like to output
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_attachment_headers = (attachment: MessageAttachment) => {
|
||||
const outputAttachmentHeaders = (attachment: MessageAttachment) => {
|
||||
let data: string[] = [];
|
||||
const headers: Partial<MessageHeaders> = {
|
||||
'content-type':
|
||||
|
@ -361,7 +401,7 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} [callback] the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_base64 = (data: string, callback?: () => void) => {
|
||||
const outputBase64 = (data: string, callback?: () => void) => {
|
||||
const loops = Math.ceil(data.length / MIMECHUNK);
|
||||
let loop = 0;
|
||||
while (loop < loops) {
|
||||
|
@ -373,13 +413,21 @@ class MessageStream extends Stream {
|
|||
}
|
||||
};
|
||||
|
||||
const output_file = (
|
||||
const outputFile = (
|
||||
attachment: MessageAttachment,
|
||||
next: (err: NodeJS.ErrnoException | null) => void
|
||||
) => {
|
||||
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
|
||||
output_base64(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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -435,7 +474,7 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_stream = (
|
||||
const outputStream = (
|
||||
attachment: MessageAttachment,
|
||||
callback: () => void
|
||||
) => {
|
||||
|
@ -446,7 +485,7 @@ class MessageStream extends Stream {
|
|||
stream.resume();
|
||||
|
||||
stream.on('end', () => {
|
||||
output_base64(previous.toString('base64'), callback);
|
||||
outputBase64(previous.toString('base64'), callback);
|
||||
this.removeListener('pause', stream.pause);
|
||||
this.removeListener('resume', stream.resume);
|
||||
this.removeListener('error', stream.resume);
|
||||
|
@ -468,7 +507,7 @@ class MessageStream extends Stream {
|
|||
// copy dangling bytes into previous buffer
|
||||
buffer.copy(previous, 0, buffer.length - padded);
|
||||
}
|
||||
output_base64(buffer.toString('base64', 0, buffer.length - padded));
|
||||
outputBase64(buffer.toString('base64', 0, buffer.length - padded));
|
||||
});
|
||||
|
||||
this.on('pause', stream.pause);
|
||||
|
@ -479,16 +518,16 @@ class MessageStream extends Stream {
|
|||
}
|
||||
};
|
||||
|
||||
const output_attachment = (
|
||||
const outputAttachment = (
|
||||
attachment: MessageAttachment,
|
||||
callback: () => void
|
||||
) => {
|
||||
const build = attachment.path
|
||||
? output_file
|
||||
? outputFile
|
||||
: attachment.stream
|
||||
? output_stream
|
||||
: output_data;
|
||||
output_attachment_headers(attachment);
|
||||
? outputStream
|
||||
: outputData;
|
||||
outputAttachmentHeaders(attachment);
|
||||
build(attachment, callback);
|
||||
};
|
||||
|
||||
|
@ -499,7 +538,7 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call if index is greater than upper bound
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_message = (
|
||||
const outputMessage = (
|
||||
boundary: string,
|
||||
list: MessageAttachment[],
|
||||
index: number,
|
||||
|
@ -508,12 +547,12 @@ class MessageStream extends Stream {
|
|||
if (index < list.length) {
|
||||
output(`--${boundary}${CRLF}`);
|
||||
if (list[index].related) {
|
||||
output_related(list[index], () =>
|
||||
output_message(boundary, list, index + 1, callback)
|
||||
outputRelated(list[index], () =>
|
||||
outputMessage(boundary, list, index + 1, callback)
|
||||
);
|
||||
} else {
|
||||
output_attachment(list[index], () =>
|
||||
output_message(boundary, list, index + 1, callback)
|
||||
outputAttachment(list[index], () =>
|
||||
outputMessage(boundary, list, index + 1, callback)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -522,20 +561,20 @@ class MessageStream extends Stream {
|
|||
}
|
||||
};
|
||||
|
||||
const output_mixed = () => {
|
||||
const boundary = generate_boundary();
|
||||
const outputMixed = () => {
|
||||
const boundary = generateBoundary();
|
||||
output(
|
||||
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||
);
|
||||
|
||||
if (this.message.alternative == null) {
|
||||
output_text(this.message);
|
||||
output_message(boundary, this.message.attachments, 0, close);
|
||||
outputText(this.message);
|
||||
outputMessage(boundary, this.message.attachments, 0, close);
|
||||
} else {
|
||||
output_alternative(
|
||||
outputAlternative(
|
||||
// typescript bug; should narrow to { alternative: MessageAttachment }
|
||||
this.message as Parameters<typeof output_alternative>[0],
|
||||
() => output_message(boundary, this.message.attachments, 0, close)
|
||||
this.message as Parameters<typeof outputAlternative>[0],
|
||||
() => outputMessage(boundary, this.message.attachments, 0, close)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -545,11 +584,11 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_data = (
|
||||
const outputData = (
|
||||
attachment: MessageAttachment,
|
||||
callback: () => void
|
||||
) => {
|
||||
output_base64(
|
||||
outputBase64(
|
||||
attachment.encoded
|
||||
? attachment.data ?? ''
|
||||
: Buffer.from(attachment.data ?? '').toString('base64'),
|
||||
|
@ -561,7 +600,7 @@ class MessageStream extends Stream {
|
|||
* @param {Message} message the message to output
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_text = (message: Message) => {
|
||||
const outputText = (message: Message) => {
|
||||
let data: string[] = [];
|
||||
|
||||
data = data.concat([
|
||||
|
@ -582,16 +621,16 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_related = (
|
||||
const outputRelated = (
|
||||
message: MessageAttachment,
|
||||
callback: () => void
|
||||
) => {
|
||||
const boundary = generate_boundary();
|
||||
const boundary = generateBoundary();
|
||||
output(
|
||||
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||
);
|
||||
output_attachment(message, () => {
|
||||
output_message(boundary, message.related ?? [], 0, () => {
|
||||
outputAttachment(message, () => {
|
||||
outputMessage(boundary, message.related ?? [], 0, () => {
|
||||
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
||||
callback();
|
||||
});
|
||||
|
@ -603,15 +642,15 @@ class MessageStream extends Stream {
|
|||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_alternative = (
|
||||
const outputAlternative = (
|
||||
message: Message & { alternative: MessageAttachment },
|
||||
callback: () => void
|
||||
) => {
|
||||
const boundary = generate_boundary();
|
||||
const boundary = generateBoundary();
|
||||
output(
|
||||
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||
);
|
||||
output_text(message);
|
||||
outputText(message);
|
||||
output(`--${boundary}${CRLF}`);
|
||||
|
||||
/**
|
||||
|
@ -623,9 +662,9 @@ class MessageStream extends Stream {
|
|||
};
|
||||
|
||||
if (message.alternative.related) {
|
||||
output_related(message.alternative, finish);
|
||||
outputRelated(message.alternative, finish);
|
||||
} else {
|
||||
output_attachment(message.alternative, finish);
|
||||
outputAttachment(message.alternative, finish);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -652,13 +691,13 @@ class MessageStream extends Stream {
|
|||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_header_data = () => {
|
||||
const outputHeaderData = () => {
|
||||
if (this.message.attachments.length || this.message.alternative) {
|
||||
output(`MIME-Version: 1.0${CRLF}`);
|
||||
output_mixed();
|
||||
outputMixed();
|
||||
} // you only have a text message!
|
||||
else {
|
||||
output_text(this.message);
|
||||
outputText(this.message);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
@ -666,7 +705,7 @@ class MessageStream extends Stream {
|
|||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_header = () => {
|
||||
const outputHeader = () => {
|
||||
let data: string[] = [];
|
||||
|
||||
for (const header in this.message.header) {
|
||||
|
@ -685,11 +724,11 @@ class MessageStream extends Stream {
|
|||
}
|
||||
|
||||
output(data.join(''));
|
||||
output_header_data();
|
||||
outputHeaderData();
|
||||
};
|
||||
|
||||
this.once('destroy', close);
|
||||
process.nextTick(output_header);
|
||||
process.nextTick(outputHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'), [
|
||||
|
|
38
test/auth.ts
38
test/auth.ts
|
@ -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((resolve, reject) => {
|
||||
t.plan(5);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
|
175
test/client.ts
175
test/client.ts
|
@ -1,14 +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 { DEFAULT_TIMEOUT, SMTPClient, Message, MessageHeaders } from '../email';
|
||||
import type { MessageHeaders } from '../email.js';
|
||||
import {
|
||||
DEFAULT_TIMEOUT,
|
||||
SMTPClient,
|
||||
Message,
|
||||
isRFC2822Date,
|
||||
} 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,
|
||||
|
@ -43,7 +50,7 @@ async function send(headers: Partial<MessageHeaders>) {
|
|||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(parseMap.get(headers.subject as string));
|
||||
resolve(parseMap.get(headers.subject as string) as ParsedMail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -57,19 +64,40 @@ test.after(async (t) => {
|
|||
});
|
||||
|
||||
test('client invokes callback exactly once for invalid connection', async (t) => {
|
||||
t.plan(1);
|
||||
const msg = {
|
||||
from: 'foo@bar.baz',
|
||||
to: 'foo@bar.baz',
|
||||
subject: 'hello world',
|
||||
text: 'hello world',
|
||||
};
|
||||
try {
|
||||
const invalidClient = new SMTPClient({ host: 'bar.baz' });
|
||||
await promisify(invalidClient.send.bind(invalidClient))(new Message(msg));
|
||||
} catch (err) {
|
||||
t.true(err instanceof Error);
|
||||
}
|
||||
await t.notThrowsAsync(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let counter = 0;
|
||||
const invalidClient = new SMTPClient({ host: 'localhost' });
|
||||
const incrementCounter = () => {
|
||||
if (counter > 0) {
|
||||
reject();
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
};
|
||||
invalidClient.send(new Message(msg), (err) => {
|
||||
if (err == null) {
|
||||
reject();
|
||||
} else {
|
||||
incrementCounter();
|
||||
}
|
||||
});
|
||||
// @ts-expect-error the error event is only accessible from the protected socket property
|
||||
invalidClient.smtp.sock.once('error', () => {
|
||||
if (counter === 1) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('client has a default connection timeout', async (t) => {
|
||||
|
@ -113,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);
|
||||
|
@ -131,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) => {
|
||||
|
@ -168,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) => {
|
||||
|
@ -228,19 +259,19 @@ 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);
|
||||
};
|
||||
|
||||
const p = greylistPort++;
|
||||
await t.notThrowsAsync(
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise<void>((resolve, reject) => {
|
||||
greylistServer.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
|
@ -275,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) {
|
||||
|
@ -288,8 +319,8 @@ test('client only responds once to greylisting', async (t) => {
|
|||
});
|
||||
|
||||
const p = greylistPort++;
|
||||
const { message: error } = await t.throwsAsync(
|
||||
new Promise((resolve, reject) => {
|
||||
const error = await t.throwsAsync(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
greylistServer.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
|
@ -307,5 +338,97 @@ 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) => {
|
||||
// bind necessary to retain internal access to client prototype
|
||||
const sendAsync = promisify(client.send.bind(client));
|
||||
|
||||
const msg = {
|
||||
subject: 'this is a test TEXT message from emailjs',
|
||||
from: 'piglet@gmail.com',
|
||||
bcc: 'pooh@gmail.com',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
try {
|
||||
const message = (await sendAsync(new Message(msg))) as Message;
|
||||
t.true(message instanceof Message);
|
||||
t.like(message, {
|
||||
alternative: null,
|
||||
content: 'text/plain; charset=utf-8',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
header: {
|
||||
bcc: 'pooh@gmail.com',
|
||||
from: 'piglet@gmail.com',
|
||||
subject: '=?UTF-8?Q?this_is_a_test_TEXT_message_from_emailjs?=',
|
||||
},
|
||||
});
|
||||
t.deepEqual(message.attachments, []);
|
||||
t.true(isRFC2822Date(message.header.date as string));
|
||||
t.regex(message.header['message-id'] as string, /^<.*[@]{1}.*>$/);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
t.fail(err.message);
|
||||
} else if (typeof err === 'string') {
|
||||
t.fail(err);
|
||||
} else {
|
||||
t.fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('client sendAsync can have result awaited', async (t) => {
|
||||
const msg = {
|
||||
subject: 'this is a test TEXT message from emailjs',
|
||||
from: 'piglet@gmail.com',
|
||||
bcc: 'pooh@gmail.com',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
try {
|
||||
const message = await client.sendAsync(new Message(msg));
|
||||
t.true(message instanceof Message);
|
||||
t.like(message, {
|
||||
alternative: null,
|
||||
content: 'text/plain; charset=utf-8',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
header: {
|
||||
bcc: 'pooh@gmail.com',
|
||||
from: 'piglet@gmail.com',
|
||||
subject: '=?UTF-8?Q?this_is_a_test_TEXT_message_from_emailjs?=',
|
||||
},
|
||||
});
|
||||
t.deepEqual(message.attachments, []);
|
||||
t.true(isRFC2822Date(message.header.date as string));
|
||||
t.regex(message.header['message-id'] as string, /^<.*[@]{1}.*>$/);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
t.fail(err.message);
|
||||
} else if (typeof err === 'string') {
|
||||
t.fail(err);
|
||||
} else {
|
||||
t.fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('client sendAsync can have error caught when awaited', async (t) => {
|
||||
const msg = {
|
||||
subject: 'this is a test TEXT message from emailjs',
|
||||
from: 'piglet@gmail.com',
|
||||
bcc: 'pooh@gmail.com',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
try {
|
||||
const invalidClient = new SMTPClient({ host: '127.0.0.1' });
|
||||
const message = await invalidClient.sendAsync(new Message(msg));
|
||||
t.true(message instanceof Message);
|
||||
t.fail();
|
||||
} catch (err) {
|
||||
t.true(err instanceof Error);
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
15
test/date.ts
15
test/date.ts
|
@ -1,18 +1,15 @@
|
|||
import test from 'ava';
|
||||
import { getRFC2822Date, getRFC2822DateUTC } 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);
|
||||
|
||||
test('rfc2822 non-UTC', async (t) => {
|
||||
// RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3
|
||||
// thanks to moment.js for the listing: 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}))$/;
|
||||
t.regex(toD(0), rfc2822re);
|
||||
t.regex(toD(329629726785), rfc2822re);
|
||||
t.regex(toD(729629726785), rfc2822re);
|
||||
t.regex(toD(1129629726785), rfc2822re);
|
||||
t.regex(toD(1529629726785), rfc2822re);
|
||||
t.true(isRFC2822Date(toD(0)));
|
||||
t.true(isRFC2822Date(toD(329629726785)));
|
||||
t.true(isRFC2822Date(toD(729629726785)));
|
||||
t.true(isRFC2822Date(toD(1129629726785)));
|
||||
t.true(isRFC2822Date(toD(1529629726785)));
|
||||
});
|
||||
|
||||
test('rfc2822 UTC', async (t) => {
|
||||
|
|
231
test/message.ts
231
test/message.ts
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue