mirror of
https://github.com/eleith/emailjs.git
synced 2024-06-18 05:39:03 +00:00
Merge pull request #246 from eleith/ø
migrate to typescript & node.js-compatible es modules
This commit is contained in:
commit
a9d6878625
|
@ -1,17 +1,33 @@
|
||||||
{
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 8,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true
|
||||||
|
},
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"es6": true,
|
|
||||||
"mocha": true,
|
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"mocha"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"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",
|
||||||
|
{
|
||||||
|
"ignoreRestArgs": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"curly": [
|
"curly": [
|
||||||
"error",
|
"error",
|
||||||
"all"
|
"all"
|
||||||
|
@ -30,11 +46,6 @@
|
||||||
"ignoreRestSiblings": true
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"valid-jsdoc": "error",
|
"valid-jsdoc": "error"
|
||||||
"mocha/handle-done-callback": "error",
|
|
||||||
"mocha/no-exclusive-tests": "error",
|
|
||||||
"mocha/no-global-tests": "error",
|
|
||||||
"mocha/no-mocha-arrows": "error",
|
|
||||||
"mocha/no-skipped-tests": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
|
@ -11,18 +11,21 @@ jobs:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: node
|
- name: node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: install
|
- name: install
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
|
- name: test-cjs
|
||||||
|
run: yarn test-cjs
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,7 +1,7 @@
|
||||||
/.vscode/
|
.vscode
|
||||||
/node_modules/
|
node_modules
|
||||||
/test/config.js
|
|
||||||
/npm-debug.log
|
*.log
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
10
.npmignore
10
.npmignore
|
@ -1,10 +0,0 @@
|
||||||
/node_modules/
|
|
||||||
/test/
|
|
||||||
/rollup/
|
|
||||||
/rollup.config.js
|
|
||||||
/email.esm.js
|
|
||||||
|
|
||||||
/.gitignore
|
|
||||||
/.npmignore
|
|
||||||
/.travis.yml
|
|
||||||
/*.log
|
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": true
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "6"
|
|
||||||
- "8"
|
|
||||||
- "10"
|
|
||||||
script:
|
|
||||||
- npm run test
|
|
||||||
- npm run rollup
|
|
193
Readme.md
193
Readme.md
|
@ -1,4 +1,4 @@
|
||||||
# emailjs [![Build Status](https://secure.travis-ci.org/eleith/emailjs.png)](http://travis-ci.org/eleith/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)
|
||||||
|
|
||||||
send emails, html and attachments (files, streams and strings) from node.js to any smtp server
|
send emails, html and attachments (files, streams and strings) from node.js to any smtp server
|
||||||
|
|
||||||
|
@ -21,119 +21,129 @@ send emails, html and attachments (files, streams and strings) from node.js to a
|
||||||
## EXAMPLE USAGE - text only emails
|
## EXAMPLE USAGE - text only emails
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var email = require("./path/to/emailjs/email");
|
import { client as c } from 'emailjs';
|
||||||
var server = email.server.connect({
|
|
||||||
user: "username",
|
const client = new c.Client({
|
||||||
password:"password",
|
user: 'user',
|
||||||
host: "smtp.your-email.com",
|
password: 'password',
|
||||||
ssl: true
|
host: 'smtp.your-email.com',
|
||||||
|
ssl: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// send the message and get a callback with an error or details of the message that was sent
|
// send the message and get a callback with an error or details of the message that was sent
|
||||||
server.send({
|
client.send({
|
||||||
text: "i hope this works",
|
text: 'i hope this works',
|
||||||
from: "you <username@your-email.com>",
|
from: 'you <username@your-email.com>',
|
||||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||||
cc: "else <else@your-email.com>",
|
cc: 'else <else@your-email.com>',
|
||||||
subject: "testing emailjs"
|
subject: 'testing emailjs'
|
||||||
}, function(err, message) { console.log(err || message); });
|
}, (err, message) => {
|
||||||
|
console.log(err || message);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## EXAMPLE USAGE - html emails and attachments
|
## EXAMPLE USAGE - html emails and attachments
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var email = require("./path/to/emailjs/email");
|
import { client as c } from 'emailjs';
|
||||||
var server = email.server.connect({
|
|
||||||
user: "username",
|
const client = new c.Client({
|
||||||
password:"password",
|
user: 'user',
|
||||||
host: "smtp.your-email.com",
|
password: 'password',
|
||||||
ssl: true
|
host: 'smtp.your-email.com',
|
||||||
|
ssl: true
|
||||||
});
|
});
|
||||||
|
|
||||||
var message = {
|
const message = {
|
||||||
text: "i hope this works",
|
text: 'i hope this works',
|
||||||
from: "you <username@your-email.com>",
|
from: 'you <username@your-email.com>',
|
||||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||||
cc: "else <else@your-email.com>",
|
cc: 'else <else@your-email.com>',
|
||||||
subject: "testing emailjs",
|
subject: 'testing emailjs',
|
||||||
attachment:
|
attachment: [
|
||||||
[
|
{ data: '<html>i <i>hope</i> this works!</html>', alternative: true },
|
||||||
{data:"<html>i <i>hope</i> this works!</html>", alternative:true},
|
{ path: 'path/to/file.zip', type: 'application/zip', name: 'renamed.zip' }
|
||||||
{path:"path/to/file.zip", type:"application/zip", name:"renamed.zip"}
|
]
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// send the message and get a callback with an error or details of the message that was sent
|
// send the message and get a callback with an error or details of the message that was sent
|
||||||
server.send(message, function(err, message) { console.log(err || message); });
|
client.send(message, function(err, message) { console.log(err || message); });
|
||||||
|
|
||||||
// you can continue to send more messages with successive calls to 'server.send',
|
// you can continue to send more messages with successive calls to 'client.send',
|
||||||
// they will be queued on the same smtp connection
|
// they will be queued on the same smtp connection
|
||||||
|
|
||||||
// or you can create a new server connection with 'email.server.connect'
|
// or instead of using the built-in client you can create an instance of 'smtp.SMTPConnection'
|
||||||
// to asynchronously send individual emails instead of a queue
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## EXAMPLE USAGE - sending through hotmail/outlook
|
## EXAMPLE USAGE - sending through outlook
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var email = require("./path/to/emailjs/email");
|
import { client as c, message as m } from 'emailjs';
|
||||||
var server = email.server.connect({
|
|
||||||
user: "username",
|
const client = new c.Client({
|
||||||
password:"password",
|
user: 'user',
|
||||||
host: "smtp-mail.outlook.com",
|
password: 'password',
|
||||||
tls: {ciphers: "SSLv3"}
|
host: 'smtp-mail.outlook.com',
|
||||||
|
tls: {
|
||||||
|
ciphers: 'SSLv3'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var message = {
|
const message = new m.Message({
|
||||||
text: "i hope this works",
|
text: 'i hope this works',
|
||||||
from: "you <username@outlook.com>",
|
from: 'you <username@outlook.com>',
|
||||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||||
cc: "else <else@your-email.com>",
|
cc: 'else <else@your-email.com>',
|
||||||
subject: "testing emailjs",
|
subject: 'testing emailjs',
|
||||||
attachment:
|
attachment: [
|
||||||
[
|
{ data: '<html>i <i>hope</i> this works!</html>', alternative: true },
|
||||||
{data:"<html>i <i>hope</i> this works!</html>", alternative:true},
|
{ path: 'path/to/file.zip', type: 'application/zip', name: 'renamed.zip' }
|
||||||
{path:"path/to/file.zip", type:"application/zip", name:"renamed.zip"}
|
]
|
||||||
]
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// send the message and get a callback with an error or details of the message that was sent
|
// send the message and get a callback with an error or details of the message that was sent
|
||||||
server.send(message, function(err, message) { console.log(err || message); });
|
client.send(message, (err, message) => {
|
||||||
|
console.log(err || message);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## EXAMPLE USAGE - attaching and embedding an image
|
## EXAMPLE USAGE - attaching and embedding an image
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var email = require("./path/to/emailjs/email");
|
import { client as c, message as m } from 'emailjs';
|
||||||
var server = email.server.connect({
|
|
||||||
user: "username",
|
const client = new c.Client({
|
||||||
password:"password",
|
user: 'user',
|
||||||
host: "smtp-mail.outlook.com",
|
password: 'password',
|
||||||
tls: {ciphers: "SSLv3"}
|
host: 'smtp-mail.outlook.com',
|
||||||
|
tls: {
|
||||||
|
ciphers: 'SSLv3'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var message = {
|
const message = new m.Message({
|
||||||
text: "i hope this works",
|
text: 'i hope this works',
|
||||||
from: "you <username@outlook.com>",
|
from: 'you <username@outlook.com>',
|
||||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||||
cc: "else <else@your-email.com>",
|
cc: 'else <else@your-email.com>',
|
||||||
subject: "testing emailjs",
|
subject: 'testing emailjs',
|
||||||
attachment:
|
attachment: [
|
||||||
[
|
{ data: '<html>i <i>hope</i> this works! here is an image: <img src="cid:my-image" width="100" height ="50"> </html>' },
|
||||||
{data: "<html>i <i>hope</i> this works! here is an image: <img src='cid:my-image' width='100' height ='50'> </html>"},
|
{ path: 'path/to/file.zip', type: 'application/zip', name: 'renamed.zip' },
|
||||||
{path:"path/to/file.zip", type:"application/zip", name:"renamed.zip"},
|
{ path: 'path/to/image.jpg', type: 'image/jpg', headers: { 'Content-ID': '<my-image>' } }
|
||||||
{path:"path/to/image.jpg", type:"image/jpg", headers:{"Content-ID":"<my-image>"}}
|
]
|
||||||
]
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// send the message and get a callback with an error or details of the message that was sent
|
// send the message and get a callback with an error or details of the message that was sent
|
||||||
server.send(message, function(err, message) { console.log(err || message); });
|
client.send(message, (err, message) => {
|
||||||
|
console.log(err || message);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
||||||
## email.server.connect(options)
|
## new client.Client(options)
|
||||||
|
|
||||||
// options is an object with the following keys
|
// options is an object with the following keys
|
||||||
options =
|
options =
|
||||||
|
@ -150,7 +160,7 @@ server.send(message, function(err, message) { console.log(err || message); });
|
||||||
logger // override the built-in logger (useful for e.g. Azure Function Apps, where console.log doesn't work)
|
logger // override the built-in logger (useful for e.g. Azure Function Apps, where console.log doesn't work)
|
||||||
}
|
}
|
||||||
|
|
||||||
## email.server.send(message, callback)
|
## client.Client#send(message, callback)
|
||||||
|
|
||||||
// message can be a smtp.Message (as returned by email.message.create)
|
// message can be a smtp.Message (as returned by email.message.create)
|
||||||
// or an object identical to the first argument accepted by email.message.create
|
// or an object identical to the first argument accepted by email.message.create
|
||||||
|
@ -158,7 +168,7 @@ server.send(message, function(err, message) { console.log(err || message); });
|
||||||
// callback will be executed with (err, message)
|
// callback will be executed with (err, message)
|
||||||
// either when message is sent or an error has occurred
|
// either when message is sent or an error has occurred
|
||||||
|
|
||||||
## message
|
## new message.Message(headers)
|
||||||
|
|
||||||
// headers is an object ('from' and 'to' are required)
|
// headers is an object ('from' and 'to' are required)
|
||||||
// returns a Message object
|
// returns a Message object
|
||||||
|
@ -177,11 +187,7 @@ server.send(message, function(err, message) { console.log(err || message); });
|
||||||
attachment // one attachment or array of attachments
|
attachment // one attachment or array of attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
## email.SMTP.authentication
|
## message.Message#attach
|
||||||
|
|
||||||
associative array of currently supported SMTP authentication mechanisms
|
|
||||||
|
|
||||||
## attachment
|
|
||||||
|
|
||||||
// can be called multiple times, each adding a new attachment
|
// can be called multiple times, each adding a new attachment
|
||||||
// options is an object with the following possible keys:
|
// options is an object with the following possible keys:
|
||||||
|
@ -207,6 +213,27 @@ associative array of currently supported SMTP authentication mechanisms
|
||||||
related // an array of attachments that you want to be related to the parent attachment
|
related // an array of attachments that you want to be related to the parent attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## new smtp.SMTPConnection(options)
|
||||||
|
|
||||||
|
// options is an object with the following keys
|
||||||
|
options =
|
||||||
|
{
|
||||||
|
user // username for logging into smtp
|
||||||
|
password // password for logging into smtp
|
||||||
|
host // smtp host
|
||||||
|
port // smtp port (if null a standard port number will be used)
|
||||||
|
ssl // boolean or object {key, ca, cert} (if true or object, ssl connection will be made)
|
||||||
|
tls // boolean or object (if true or object, starttls will be initiated)
|
||||||
|
timeout // max number of milliseconds to wait for smtp responses (defaults to 5000)
|
||||||
|
domain // domain to greet smtp with (defaults to os.hostname)
|
||||||
|
authentication // array of preferred authentication methods ('PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2')
|
||||||
|
logger // override the built-in logger (useful for e.g. Azure Function Apps, where console.log doesn't work)
|
||||||
|
}
|
||||||
|
|
||||||
|
## smtp.SMTPConnection#authentication
|
||||||
|
|
||||||
|
associative array of currently supported SMTP authentication mechanisms
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
eleith
|
eleith
|
||||||
|
|
8
ava.config.js
Normal file
8
ava.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default {
|
||||||
|
files: ['test/*.ts'],
|
||||||
|
extensions: ['ts'],
|
||||||
|
require: ['./email.test.ts'],
|
||||||
|
environmentVariables: {
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,7 +0,0 @@
|
||||||
import * as server from './smtp/client';
|
|
||||||
import * as message from './smtp/message';
|
|
||||||
import * as date from './smtp/date';
|
|
||||||
import * as SMTP from './smtp/smtp';
|
|
||||||
import * as error from './smtp/error';
|
|
||||||
|
|
||||||
export { server, message, date, SMTP, error };
|
|
5
email.js
5
email.js
|
@ -1,5 +0,0 @@
|
||||||
exports.server = require('./smtp/client');
|
|
||||||
exports.message = require('./smtp/message');
|
|
||||||
exports.date = require('./smtp/date');
|
|
||||||
exports.SMTP = require('./smtp/smtp');
|
|
||||||
exports.error = require('./smtp/error');
|
|
7
email.test.ts
Normal file
7
email.test.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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');
|
||||||
|
}
|
5
email.ts
Normal file
5
email.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * as client from './smtp/client';
|
||||||
|
export * as message from './smtp/message';
|
||||||
|
export * as date from './smtp/date';
|
||||||
|
export * as smtp from './smtp/smtp';
|
||||||
|
export * as error from './smtp/error';
|
115
package.json
115
package.json
|
@ -1,55 +1,64 @@
|
||||||
{
|
{
|
||||||
"name": "emailjs",
|
"name": "emailjs",
|
||||||
"description": "send text/html emails and attachments (files, streams and strings) from node.js to any smtp server",
|
"description": "send text/html emails and attachments (files, streams and strings) from node.js to any smtp server",
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"author": "eleith",
|
"author": "eleith",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"izuzak",
|
"izuzak",
|
||||||
"Hiverness",
|
"Hiverness",
|
||||||
"mscdex",
|
"mscdex",
|
||||||
"jimmybergman",
|
"jimmybergman",
|
||||||
"zackschuster"
|
"zackschuster"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/eleith/emailjs.git"
|
"url": "http://github.com/eleith/emailjs.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"addressparser": "^0.3.2",
|
"addressparser": "1.0.1",
|
||||||
"emailjs-mime-codec": "^2.0.7"
|
"emailjs-mime-codec": "2.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"type": "module",
|
||||||
"chai": "^4.1.2",
|
"devDependencies": {
|
||||||
"eslint": "^5.1.0",
|
"@ledge/configs": "23.0.0",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"@rollup/plugin-commonjs": "12.0.0",
|
||||||
"eslint-plugin-mocha": "^5.1.0",
|
"@rollup/plugin-node-resolve": "8.0.0",
|
||||||
"eslint-plugin-prettier": "^2.6.2",
|
"@rollup/plugin-typescript": "4.1.2",
|
||||||
"mailparser": "^2.2.0",
|
"@types/mailparser": "2.7.3",
|
||||||
"mocha": "^5.2.0",
|
"@types/smtp-server": "3.5.4",
|
||||||
"prettier": "^1.13.7",
|
"@typescript-eslint/eslint-plugin": "3.0.1",
|
||||||
"rollup": "^0.62.0",
|
"@typescript-eslint/parser": "3.0.1",
|
||||||
"rollup-plugin-commonjs": "^9.1.3",
|
"ava": "3.8.2",
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
"eslint": "7.1.0",
|
||||||
"smtp-server": "^3.4.6"
|
"eslint-config-prettier": "6.11.0",
|
||||||
},
|
"eslint-plugin-prettier": "3.1.3",
|
||||||
"engine": [
|
"mailparser": "2.7.7",
|
||||||
"node >= 6"
|
"prettier": "2.0.5",
|
||||||
],
|
"rollup": "2.10.9",
|
||||||
"main": "email.js",
|
"smtp-server": "3.6.0",
|
||||||
"scripts": {
|
"ts-node": "8.10.1",
|
||||||
"rollup": "rollup -c rollup.config.js && npm run rollup:test",
|
"tslib": "2.0.0",
|
||||||
"rollup:test": "npm run test -- --file rollup/email.bundle.test.js",
|
"typescript": "3.9.3"
|
||||||
"test": "mocha"
|
},
|
||||||
},
|
"engine": [
|
||||||
"license": "MIT",
|
"node >= 10"
|
||||||
"eslintIgnore": [
|
],
|
||||||
"rollup.config.js",
|
"files": [
|
||||||
"rollup/email.bundle.js",
|
"email.ts",
|
||||||
"email.esm.js"
|
"smtp",
|
||||||
],
|
"rollup"
|
||||||
"prettier": {
|
],
|
||||||
"singleQuote": true,
|
"main": "./rollup/email.cjs",
|
||||||
"trailingComma": "es5",
|
"types": "./email.ts",
|
||||||
"useTabs": true
|
"exports": {
|
||||||
}
|
"import": "./rollup/email.mjs",
|
||||||
|
"require": "./rollup/email.cjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c rollup.config.ts",
|
||||||
|
"lint": "eslint *.ts \"+(smtp|test)/*.ts\"",
|
||||||
|
"test": "ava --serial",
|
||||||
|
"test-cjs": "npm run build && npm run test -- --node-arguments='--title=cjs'"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import commonjs from 'rollup-plugin-commonjs';
|
|
||||||
import resolve from 'rollup-plugin-node-resolve';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'email.esm.js',
|
|
||||||
output: {
|
|
||||||
file: 'rollup/email.bundle.js',
|
|
||||||
format: 'cjs',
|
|
||||||
interop: false,
|
|
||||||
freeze: false,
|
|
||||||
},
|
|
||||||
external: require('module').builtinModules,
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
]
|
|
||||||
};
|
|
30
rollup.config.ts
Normal file
30
rollup.config.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import module from 'module';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
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,
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
|
typescript({ removeComments: false, include: ['email.ts', 'smtp/*.ts'] }),
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,4 +0,0 @@
|
||||||
require('./email.bundle.js');
|
|
||||||
require.cache[require.resolve('../email.js')] =
|
|
||||||
require.cache[require.resolve('./email.bundle.js')];
|
|
||||||
console.log('Testing email.bundle.js...');
|
|
File diff suppressed because it is too large
Load Diff
1
rollup/email.cjs.map
Normal file
1
rollup/email.cjs.map
Normal file
File diff suppressed because one or more lines are too long
16124
rollup/email.mjs
Normal file
16124
rollup/email.mjs
Normal file
File diff suppressed because one or more lines are too long
1
rollup/email.mjs.map
Normal file
1
rollup/email.mjs.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,87 +1,53 @@
|
||||||
const { SMTP, state } = require('./smtp');
|
import addressparser from 'addressparser';
|
||||||
const { Message, create } = require('./message');
|
import { Message } from './message';
|
||||||
const addressparser = require('addressparser');
|
import type { MessageAttachment, MessageHeaders } from './message';
|
||||||
|
import { SMTPConnection, SMTPState } from './smtp';
|
||||||
|
import type { SMTPConnectionOptions } from './smtp';
|
||||||
|
|
||||||
|
export interface MessageStack {
|
||||||
|
callback: (error: Error | null, message: Message) => void;
|
||||||
|
message: Message;
|
||||||
|
attachment: MessageAttachment;
|
||||||
|
text: string;
|
||||||
|
returnPath: string;
|
||||||
|
from: string;
|
||||||
|
to: ReturnType<typeof addressparser>;
|
||||||
|
cc: string[];
|
||||||
|
bcc: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
public readonly smtp: SMTPConnection;
|
||||||
|
public readonly queue: MessageStack[] = [];
|
||||||
|
|
||||||
|
protected sending = false;
|
||||||
|
protected ready = false;
|
||||||
|
protected timer: NodeJS.Timer | null = null;
|
||||||
|
|
||||||
class Client {
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} MessageStack
|
* @param {SMTPConnectionOptions} server smtp options
|
||||||
* @property {function(Error, Message): void} [callback]
|
|
||||||
* @property {Message} [message]
|
|
||||||
* @property {string} [returnPath]
|
|
||||||
* @property {string} [from]
|
|
||||||
* @property {string} [subject]
|
|
||||||
* @property {string|Array} [to]
|
|
||||||
* @property {Array} [cc]
|
|
||||||
* @property {Array} [bcc]
|
|
||||||
* @property {string} [text]
|
|
||||||
* @property {*} [attachment]
|
|
||||||
*
|
|
||||||
* @typedef {Object} SMTPSocketOptions
|
|
||||||
* @property {string} key
|
|
||||||
* @property {string} ca
|
|
||||||
* @property {string} cert
|
|
||||||
*
|
|
||||||
* @typedef {Object} SMTPOptions
|
|
||||||
* @property {number} [timeout]
|
|
||||||
* @property {string} [user]
|
|
||||||
* @property {string} [password]
|
|
||||||
* @property {string} [domain]
|
|
||||||
* @property {string} [host]
|
|
||||||
* @property {number} [port]
|
|
||||||
* @property {boolean|SMTPSocketOptions} [ssl]
|
|
||||||
* @property {boolean|SMTPSocketOptions} [tls]
|
|
||||||
* @property {string[]} [authentication]
|
|
||||||
* @property {function(...any): void} [logger]
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {SMTPOptions} server smtp options
|
|
||||||
*/
|
*/
|
||||||
constructor(server) {
|
constructor(server: Partial<SMTPConnectionOptions>) {
|
||||||
this.smtp = new SMTP(server);
|
this.smtp = new SMTPConnection(server);
|
||||||
//this.smtp.debug(1);
|
//this.smtp.debug(1);
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {MessageStack[]}
|
|
||||||
*/
|
|
||||||
this.queue = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {NodeJS.Timer}
|
|
||||||
*/
|
|
||||||
this.timer = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.sending = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.ready = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Message|MessageStack} msg msg
|
* @public
|
||||||
* @param {function(Error, MessageStack): void} callback callback
|
* @param {Message} msg the message to send
|
||||||
|
* @param {function(err: Error, msg: Message): void} callback sss
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
send(msg, callback) {
|
public send(msg: Message, callback: (err: Error, msg: Message) => void) {
|
||||||
/**
|
const message: Message | null =
|
||||||
* @type {Message}
|
|
||||||
*/
|
|
||||||
const message =
|
|
||||||
msg instanceof Message
|
msg instanceof Message
|
||||||
? msg
|
? msg
|
||||||
: this._canMakeMessage(msg)
|
: this._canMakeMessage(msg)
|
||||||
? create(msg)
|
? new Message(msg)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
callback(
|
callback(new Error('message is not a valid Message instance'), msg);
|
||||||
new Error('message is not a valid Message instance'),
|
|
||||||
/** @type {MessageStack} */ (msg)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +57,13 @@ class Client {
|
||||||
message,
|
message,
|
||||||
to: addressparser(message.header.to),
|
to: addressparser(message.header.to),
|
||||||
from: addressparser(message.header.from)[0].address,
|
from: addressparser(message.header.from)[0].address,
|
||||||
callback: (callback || function() {}).bind(this),
|
callback: (
|
||||||
};
|
callback ||
|
||||||
|
function () {
|
||||||
|
/* ø */
|
||||||
|
}
|
||||||
|
).bind(this),
|
||||||
|
} as MessageStack;
|
||||||
|
|
||||||
if (message.header.cc) {
|
if (message.header.cc) {
|
||||||
stack.to = stack.to.concat(addressparser(message.header.cc));
|
stack.to = stack.to.concat(addressparser(message.header.cc));
|
||||||
|
@ -114,49 +85,51 @@ class Client {
|
||||||
this.queue.push(stack);
|
this.queue.push(stack);
|
||||||
this._poll();
|
this._poll();
|
||||||
} else {
|
} else {
|
||||||
callback(new Error(why), /** @type {MessageStack} */ (msg));
|
callback(new Error(why), msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_poll() {
|
protected _poll() {
|
||||||
clearTimeout(this.timer);
|
if (this.timer != null) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.queue.length) {
|
if (this.queue.length) {
|
||||||
if (this.smtp.state() == state.NOTCONNECTED) {
|
if (this.smtp.state() == SMTPState.NOTCONNECTED) {
|
||||||
this._connect(this.queue[0]);
|
this._connect(this.queue[0]);
|
||||||
} else if (
|
} else if (
|
||||||
this.smtp.state() == state.CONNECTED &&
|
this.smtp.state() == SMTPState.CONNECTED &&
|
||||||
!this.sending &&
|
!this.sending &&
|
||||||
this.ready
|
this.ready
|
||||||
) {
|
) {
|
||||||
this._sendmail(this.queue.shift());
|
this._sendmail(this.queue.shift() as MessageStack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// wait around 1 seconds in case something does come in,
|
// wait around 1 seconds in case something does come in,
|
||||||
// otherwise close out SMTP connection if still open
|
// otherwise close out SMTP connection if still open
|
||||||
else if (this.smtp.state() == state.CONNECTED) {
|
else if (this.smtp.state() == SMTPState.CONNECTED) {
|
||||||
this.timer = setTimeout(() => this.smtp.quit(), 1000);
|
this.timer = setTimeout(() => this.smtp.quit(), 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_connect(stack) {
|
protected _connect(stack: MessageStack) {
|
||||||
/**
|
/**
|
||||||
* @param {Error} err callback error
|
* @param {Error} err callback error
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const connect = err => {
|
const connect = (err: Error) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
const begin = err => {
|
const begin = (err: Error) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
this._poll();
|
this._poll();
|
||||||
|
@ -188,11 +161,11 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} msg message stack
|
* @param {MessageStack} msg message stack
|
||||||
* @returns {boolean} can make message
|
* @returns {boolean} can make message
|
||||||
*/
|
*/
|
||||||
_canMakeMessage(msg) {
|
protected _canMakeMessage(msg: MessageHeaders) {
|
||||||
return (
|
return (
|
||||||
msg.from &&
|
msg.from &&
|
||||||
(msg.to || msg.cc || msg.bcc) &&
|
(msg.to || msg.cc || msg.bcc) &&
|
||||||
|
@ -201,13 +174,15 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {*} attachment attachment
|
* @param {*} attachment attachment
|
||||||
* @returns {boolean} does contain
|
* @returns {*} whether the attachment contains inlined html
|
||||||
*/
|
*/
|
||||||
_containsInlinedHtml(attachment) {
|
protected _containsInlinedHtml(
|
||||||
|
attachment: MessageAttachment | MessageAttachment[]
|
||||||
|
) {
|
||||||
if (Array.isArray(attachment)) {
|
if (Array.isArray(attachment)) {
|
||||||
return attachment.some(att => {
|
return attachment.some((att) => {
|
||||||
return this._isAttachmentInlinedHtml(att);
|
return this._isAttachmentInlinedHtml(att);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -216,11 +191,11 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {*} attachment attachment
|
* @param {MessageAttachment} attachment attachment
|
||||||
* @returns {boolean} is inlined
|
* @returns {boolean} whether the attachment is inlined html
|
||||||
*/
|
*/
|
||||||
_isAttachmentInlinedHtml(attachment) {
|
protected _isAttachmentInlinedHtml(attachment: MessageAttachment) {
|
||||||
return (
|
return (
|
||||||
attachment &&
|
attachment &&
|
||||||
(attachment.data || attachment.path) &&
|
(attachment.data || attachment.path) &&
|
||||||
|
@ -229,17 +204,17 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @param {function(MessageStack): void} next next
|
* @param {function(MessageStack): void} next next
|
||||||
* @returns {function(Error): void} callback
|
* @returns {function(Error): void} callback
|
||||||
*/
|
*/
|
||||||
_sendsmtp(stack, next) {
|
protected _sendsmtp(stack: MessageStack, next: (msg: MessageStack) => void) {
|
||||||
/**
|
/**
|
||||||
* @param {Error} [err] error
|
* @param {Error} [err] error
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
return err => {
|
return (err: Error) => {
|
||||||
if (!err && next) {
|
if (!err && next) {
|
||||||
next.apply(this, [stack]);
|
next.apply(this, [stack]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -251,27 +226,27 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_sendmail(stack) {
|
protected _sendmail(stack: MessageStack) {
|
||||||
const from = stack.returnPath || stack.from;
|
const from = stack.returnPath || stack.from;
|
||||||
this.sending = true;
|
this.sending = true;
|
||||||
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
|
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_sendrcpt(stack) {
|
protected _sendrcpt(stack: MessageStack) {
|
||||||
if (stack.to == null || typeof stack.to === 'string') {
|
if (stack.to == null || typeof stack.to === 'string') {
|
||||||
throw new TypeError('stack.to must be array');
|
throw new TypeError('stack.to must be array');
|
||||||
}
|
}
|
||||||
|
|
||||||
const to = stack.to.shift().address;
|
const to = stack.to.shift()?.address;
|
||||||
this.smtp.rcpt(
|
this.smtp.rcpt(
|
||||||
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
|
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
|
||||||
`<${to}>`
|
`<${to}>`
|
||||||
|
@ -279,23 +254,23 @@ class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_senddata(stack) {
|
protected _senddata(stack: MessageStack) {
|
||||||
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
|
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_sendmessage(stack) {
|
protected _sendmessage(stack: MessageStack) {
|
||||||
const stream = stack.message.stream();
|
const stream = stack.message.stream();
|
||||||
|
|
||||||
stream.on('data', data => this.smtp.message(data));
|
stream.on('data', (data) => this.smtp.message(data));
|
||||||
stream.on('end', () => {
|
stream.on('end', () => {
|
||||||
this.smtp.data_end(
|
this.smtp.data_end(
|
||||||
this._sendsmtp(stack, () => this._senddone(null, stack))
|
this._sendsmtp(stack, () => this._senddone(null, stack))
|
||||||
|
@ -304,29 +279,21 @@ class Client {
|
||||||
|
|
||||||
// there is no way to cancel a message while in the DATA portion,
|
// there is no way to cancel a message while in the DATA portion,
|
||||||
// so we have to close the socket to prevent a bad email from going out
|
// so we have to close the socket to prevent a bad email from going out
|
||||||
stream.on('error', err => {
|
stream.on('error', (err) => {
|
||||||
this.smtp.close();
|
this.smtp.close();
|
||||||
this._senddone(err, stack);
|
this._senddone(err, stack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @protected
|
||||||
* @param {Error} err err
|
* @param {Error} err err
|
||||||
* @param {MessageStack} stack stack
|
* @param {MessageStack} stack stack
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_senddone(err, stack) {
|
protected _senddone(err: Error | null, stack: MessageStack) {
|
||||||
this.sending = false;
|
this.sending = false;
|
||||||
stack.callback(err, stack.message);
|
stack.callback(err, stack.message);
|
||||||
this._poll();
|
this._poll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.Client = Client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {SMTPOptions} server smtp options
|
|
||||||
* @returns {Client} the client
|
|
||||||
*/
|
|
||||||
exports.connect = server => new Client(server);
|
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* @param {Date} [date] an optional date to convert to RFC2822 format
|
* @param {Date} [date] an optional date to convert to RFC2822 format
|
||||||
* @param {boolean} [useUtc=false] whether to parse the date as UTC (default: false)
|
* @param {boolean} [useUtc] whether to parse the date as UTC (default: false)
|
||||||
* @returns {string} the converted date
|
* @returns {string} the converted date
|
||||||
*/
|
*/
|
||||||
function getRFC2822Date(date = new Date(), useUtc = false) {
|
export function getRFC2822Date(date = new Date(), useUtc = false) {
|
||||||
if (useUtc) {
|
if (useUtc) {
|
||||||
return getRFC2822DateUTC(date);
|
return getRFC2822DateUTC(date);
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,9 @@ function getRFC2822Date(date = new Date(), useUtc = false) {
|
||||||
* @param {Date} [date] an optional date to convert to RFC2822 format (UTC)
|
* @param {Date} [date] an optional date to convert to RFC2822 format (UTC)
|
||||||
* @returns {string} the converted date
|
* @returns {string} the converted date
|
||||||
*/
|
*/
|
||||||
function getRFC2822DateUTC(date = new Date()) {
|
export function getRFC2822DateUTC(date = new Date()) {
|
||||||
const dates = date.toUTCString().split(' ');
|
const dates = date.toUTCString().split(' ');
|
||||||
dates.pop(); // remove timezone
|
dates.pop(); // remove timezone
|
||||||
dates.push('+0000');
|
dates.push('+0000');
|
||||||
return dates.join(' ');
|
return dates.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getRFC2822Date = getRFC2822Date;
|
|
||||||
exports.getRFC2822DateUTC = getRFC2822DateUTC;
|
|
|
@ -1,86 +0,0 @@
|
||||||
class SMTPError extends Error {
|
|
||||||
/**
|
|
||||||
* @param {string} message the error message
|
|
||||||
*/
|
|
||||||
constructor(message) {
|
|
||||||
super(message);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.code = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {*}
|
|
||||||
*/
|
|
||||||
this.smtp = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Error}
|
|
||||||
*/
|
|
||||||
this.previous = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} message the error message
|
|
||||||
* @param {number} code the error code
|
|
||||||
* @param {Error} [error] an error object
|
|
||||||
* @param {*} [smtp] smtp
|
|
||||||
* @returns {SMTPError} an smtp error object
|
|
||||||
*/
|
|
||||||
module.exports = function(message, code, error, smtp) {
|
|
||||||
const err = new SMTPError(
|
|
||||||
error != null && error.message ? `${message} (${error.message})` : message
|
|
||||||
);
|
|
||||||
|
|
||||||
err.code = code;
|
|
||||||
err.smtp = smtp;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
err.previous = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {1}
|
|
||||||
*/
|
|
||||||
module.exports.COULDNOTCONNECT = 1;
|
|
||||||
/**
|
|
||||||
* @type {2}
|
|
||||||
*/
|
|
||||||
module.exports.BADRESPONSE = 2;
|
|
||||||
/**
|
|
||||||
* @type {3}
|
|
||||||
*/
|
|
||||||
module.exports.AUTHFAILED = 3;
|
|
||||||
/**
|
|
||||||
* @type {4}
|
|
||||||
*/
|
|
||||||
module.exports.TIMEDOUT = 4;
|
|
||||||
/**
|
|
||||||
* @type {5}
|
|
||||||
*/
|
|
||||||
module.exports.ERROR = 5;
|
|
||||||
/**
|
|
||||||
* @type {6}
|
|
||||||
*/
|
|
||||||
module.exports.NOCONNECTION = 6;
|
|
||||||
/**
|
|
||||||
* @type {7}
|
|
||||||
*/
|
|
||||||
module.exports.AUTHNOTSUPPORTED = 7;
|
|
||||||
/**
|
|
||||||
* @type {8}
|
|
||||||
*/
|
|
||||||
module.exports.CONNECTIONCLOSED = 8;
|
|
||||||
/**
|
|
||||||
* @type {9}
|
|
||||||
*/
|
|
||||||
module.exports.CONNECTIONENDED = 9;
|
|
||||||
/**
|
|
||||||
* @type {10}
|
|
||||||
*/
|
|
||||||
module.exports.CONNECTIONAUTH = 10;
|
|
45
smtp/error.ts
Normal file
45
smtp/error.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum
|
||||||
|
*/
|
||||||
|
export const SMTPErrorStates = {
|
||||||
|
COULDNOTCONNECT: 1,
|
||||||
|
BADRESPONSE: 2,
|
||||||
|
AUTHFAILED: 3,
|
||||||
|
TIMEDOUT: 4,
|
||||||
|
ERROR: 5,
|
||||||
|
NOCONNECTION: 6,
|
||||||
|
AUTHNOTSUPPORTED: 7,
|
||||||
|
CONNECTIONCLOSED: 8,
|
||||||
|
CONNECTIONENDED: 9,
|
||||||
|
CONNECTIONAUTH: 10,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
class SMTPError extends Error {
|
||||||
|
public code: number | null = null;
|
||||||
|
public smtp: unknown = null;
|
||||||
|
public previous: Error | null = null;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSMTPError(
|
||||||
|
message: string,
|
||||||
|
code: number,
|
||||||
|
error?: Error | null,
|
||||||
|
smtp?: unknown
|
||||||
|
) {
|
||||||
|
const msg = error?.message ? `${message} (${error.message})` : message;
|
||||||
|
const err = new SMTPError(msg);
|
||||||
|
|
||||||
|
err.code = code;
|
||||||
|
err.smtp = smtp;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
err.previous = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
|
@ -1,38 +1,81 @@
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const { hostname } = require('os');
|
import type { PathLike } from 'fs';
|
||||||
const { Stream } = require('stream');
|
import { hostname } from 'os';
|
||||||
const addressparser = require('addressparser');
|
import { Stream } from 'stream';
|
||||||
const { mimeWordEncode } = require('emailjs-mime-codec');
|
import type { Duplex } from 'stream';
|
||||||
const { getRFC2822Date } = require('./date');
|
import addressparser from 'addressparser';
|
||||||
|
import { mimeWordEncode } from 'emailjs-mime-codec';
|
||||||
|
|
||||||
const CRLF = '\r\n';
|
import { getRFC2822Date } from './date';
|
||||||
|
|
||||||
|
const CRLF = '\r\n' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIME standard wants 76 char chunks when sending out.
|
* MIME standard wants 76 char chunks when sending out.
|
||||||
* @type {76}
|
|
||||||
*/
|
*/
|
||||||
const MIMECHUNK = 76;
|
export const MIMECHUNK = 76 as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* meets both base64 and mime divisibility
|
* meets both base64 and mime divisibility
|
||||||
* @type {456}
|
|
||||||
*/
|
*/
|
||||||
const MIME64CHUNK = /** @type {456} */ (MIMECHUNK * 6);
|
export const MIME64CHUNK = (MIMECHUNK * 6) as 456;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* size of the message stream buffer
|
* size of the message stream buffer
|
||||||
* @type {12768}
|
|
||||||
*/
|
*/
|
||||||
const BUFFERSIZE = /** @type {12768} */ (MIMECHUNK * 24 * 7);
|
export const BUFFERSIZE = (MIMECHUNK * 24 * 7) as 12768;
|
||||||
|
|
||||||
|
export interface MessageAttachmentHeaders {
|
||||||
|
[index: string]: string | undefined;
|
||||||
|
'content-type'?: string;
|
||||||
|
'content-transfer-encoding'?: BufferEncoding | '7bit' | '8bit';
|
||||||
|
'content-disposition'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlternateMessageAttachment {
|
||||||
|
[index: string]:
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| MessageAttachment
|
||||||
|
| MessageAttachment[]
|
||||||
|
| MessageAttachmentHeaders
|
||||||
|
| Duplex
|
||||||
|
| PathLike
|
||||||
|
| undefined;
|
||||||
|
name?: string;
|
||||||
|
headers?: MessageAttachmentHeaders;
|
||||||
|
inline: boolean;
|
||||||
|
alternative?: MessageAttachment | boolean;
|
||||||
|
related?: MessageAttachment[];
|
||||||
|
data: string;
|
||||||
|
encoded?: boolean;
|
||||||
|
stream?: Duplex;
|
||||||
|
path?: PathLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageAttachment extends AlternateMessageAttachment {
|
||||||
|
type: string;
|
||||||
|
charset: string;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageHeaders {
|
||||||
|
[index: string]: string | null | MessageAttachment | MessageAttachment[];
|
||||||
|
'content-type': string;
|
||||||
|
'message-id': string;
|
||||||
|
'return-path': string | null;
|
||||||
|
date: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
cc: string;
|
||||||
|
bcc: string;
|
||||||
|
subject: string;
|
||||||
|
text: string | null;
|
||||||
|
attachment: MessageAttachment | MessageAttachment[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string} the generated boundary
|
|
||||||
*/
|
|
||||||
function generate_boundary() {
|
function generate_boundary() {
|
||||||
let text = '';
|
let text = '';
|
||||||
const possible =
|
const possible =
|
||||||
|
@ -45,12 +88,8 @@ function generate_boundary() {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function convertPersonToAddress(person: string) {
|
||||||
* @param {string} l the person to parse into an address
|
return addressparser(person)
|
||||||
* @returns {string} the parsed address
|
|
||||||
*/
|
|
||||||
function person2address(l) {
|
|
||||||
return addressparser(l)
|
|
||||||
.map(({ name, address }) => {
|
.map(({ name, address }) => {
|
||||||
return name
|
return name
|
||||||
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
|
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
|
||||||
|
@ -59,46 +98,31 @@ function person2address(l) {
|
||||||
.join(', ');
|
.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function convertDashDelimitedTextToSnakeCase(text: string) {
|
||||||
* @param {string} header_name the header name to fix
|
return text
|
||||||
* @returns {string} the fixed header name
|
|
||||||
*/
|
|
||||||
function fix_header_name_case(header_name) {
|
|
||||||
return header_name
|
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/^(.)|-(.)/g, match => match.toUpperCase());
|
.replace(/^(.)|-(.)/g, (match) => match.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
class Message {
|
export class Message {
|
||||||
/**
|
public readonly attachments: MessageAttachment[] = [];
|
||||||
* @typedef {Object} MessageHeaders
|
public readonly header: Partial<MessageHeaders> = {
|
||||||
* @property {string?} content-type
|
'message-id': `<${new Date().getTime()}.${counter++}.${
|
||||||
* @property {string} [subject]
|
process.pid
|
||||||
* @property {string} [text]
|
}@${hostname()}>`,
|
||||||
* @property {MessageAttachment} [attachment]
|
date: getRFC2822Date(),
|
||||||
* @param {MessageHeaders} headers hash of message headers
|
};
|
||||||
*/
|
public readonly content: string = 'text/plain; charset=utf-8';
|
||||||
constructor(headers) {
|
public readonly text?: string;
|
||||||
this.attachments = [];
|
public alternative: AlternateMessageAttachment | null = null;
|
||||||
|
|
||||||
/**
|
constructor(headers: Partial<MessageHeaders>) {
|
||||||
* @type {MessageAttachment}
|
|
||||||
*/
|
|
||||||
this.alternative = null;
|
|
||||||
this.header = {
|
|
||||||
'message-id': `<${new Date().getTime()}.${counter++}.${
|
|
||||||
process.pid
|
|
||||||
}@${hostname()}>`,
|
|
||||||
date: getRFC2822Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.content = 'text/plain; charset=utf-8';
|
|
||||||
for (const header in headers) {
|
for (const header in headers) {
|
||||||
// allow user to override default content-type to override charset or send a single non-text message
|
// allow user to override default content-type to override charset or send a single non-text message
|
||||||
if (/^content-type$/i.test(header)) {
|
if (/^content-type$/i.test(header)) {
|
||||||
this.content = headers[header];
|
this.content = headers[header] as string;
|
||||||
} else if (header === 'text') {
|
} else if (header === 'text') {
|
||||||
this.text = headers[header];
|
this.text = headers[header] as string;
|
||||||
} else if (
|
} else if (
|
||||||
header === 'attachment' &&
|
header === 'attachment' &&
|
||||||
typeof headers[header] === 'object'
|
typeof headers[header] === 'object'
|
||||||
|
@ -108,13 +132,15 @@ class Message {
|
||||||
for (let i = 0; i < attachment.length; i++) {
|
for (let i = 0; i < attachment.length; i++) {
|
||||||
this.attach(attachment[i]);
|
this.attach(attachment[i]);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (attachment != null) {
|
||||||
this.attach(attachment);
|
this.attach(attachment);
|
||||||
}
|
}
|
||||||
} else if (header === 'subject') {
|
} else if (header === 'subject') {
|
||||||
this.header.subject = mimeWordEncode(headers.subject);
|
this.header.subject = mimeWordEncode(headers.subject);
|
||||||
} else if (/^(cc|bcc|to|from)/i.test(header)) {
|
} else if (/^(cc|bcc|to|from)/i.test(header)) {
|
||||||
this.header[header.toLowerCase()] = person2address(headers[header]);
|
this.header[header.toLowerCase()] = convertPersonToAddress(
|
||||||
|
headers[header] as string
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// allow any headers the user wants to set??
|
// allow any headers the user wants to set??
|
||||||
// if(/cc|bcc|to|from|reply-to|sender|subject|date|message-id/i.test(header))
|
// if(/cc|bcc|to|from|reply-to|sender|subject|date|message-id/i.test(header))
|
||||||
|
@ -124,22 +150,11 @@ class Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {MessageAttachment} options attachment options
|
* @param {MessageAttachment} options attachment options
|
||||||
* @returns {Message} the current instance for chaining
|
* @returns {Message} the current instance for chaining
|
||||||
*/
|
*/
|
||||||
attach(options) {
|
public attach(options: MessageAttachment): Message {
|
||||||
/*
|
|
||||||
legacy support, will remove eventually...
|
|
||||||
arguments -> (path, type, name, headers)
|
|
||||||
*/
|
|
||||||
if (typeof options === 'string' && arguments.length > 1) {
|
|
||||||
options = {
|
|
||||||
path: options,
|
|
||||||
type: arguments[1],
|
|
||||||
name: arguments[2],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// sender can specify an attachment as an alternative
|
// sender can specify an attachment as an alternative
|
||||||
if (options.alternative) {
|
if (options.alternative) {
|
||||||
this.alternative = options;
|
this.alternative = options;
|
||||||
|
@ -160,7 +175,7 @@ class Message {
|
||||||
* @param {string} [charset='utf-8'] the charset to encode as
|
* @param {string} [charset='utf-8'] the charset to encode as
|
||||||
* @returns {Message} the current Message instance
|
* @returns {Message} the current Message instance
|
||||||
*/
|
*/
|
||||||
attach_alternative(html, charset) {
|
attach_alternative(html: string, charset: string): Message {
|
||||||
this.alternative = {
|
this.alternative = {
|
||||||
data: html,
|
data: html,
|
||||||
charset: charset || 'utf-8',
|
charset: charset || 'utf-8',
|
||||||
|
@ -172,10 +187,11 @@ class Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(boolean, string): void} callback This callback is displayed as part of the Requester class.
|
* @param {function(boolean, string): void} callback This callback is displayed as part of the Requester class.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
valid(callback) {
|
public valid(callback: (arg0: boolean, arg1?: string) => void) {
|
||||||
if (!this.header.from) {
|
if (!this.header.from) {
|
||||||
callback(false, 'message does not have a valid sender');
|
callback(false, 'message does not have a valid sender');
|
||||||
}
|
}
|
||||||
|
@ -185,9 +201,9 @@ class Message {
|
||||||
} else if (this.attachments.length === 0) {
|
} else if (this.attachments.length === 0) {
|
||||||
callback(true, undefined);
|
callback(true, undefined);
|
||||||
} else {
|
} else {
|
||||||
const failed = [];
|
const failed: string[] = [];
|
||||||
|
|
||||||
this.attachments.forEach(attachment => {
|
this.attachments.forEach((attachment) => {
|
||||||
if (attachment.path) {
|
if (attachment.path) {
|
||||||
if (fs.existsSync(attachment.path) == false) {
|
if (fs.existsSync(attachment.path) == false) {
|
||||||
failed.push(`${attachment.path} does not exist`);
|
failed.push(`${attachment.path} does not exist`);
|
||||||
|
@ -206,132 +222,103 @@ class Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a stream of the current message
|
* @public
|
||||||
* @returns {MessageStream} a stream of the current message
|
* @returns {*} a stream of the current message
|
||||||
*/
|
*/
|
||||||
stream() {
|
public stream() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
return new MessageStream(this);
|
return new MessageStream(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(Error, string): void} callback the function to call with the error and buffer
|
* @param {function(Error, string): void} callback the function to call with the error and buffer
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
read(callback) {
|
public read(callback: (err: Error, buffer: string) => void) {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
const str = this.stream();
|
const str = this.stream();
|
||||||
str.on('data', data => (buffer += data));
|
str.on('data', (data) => (buffer += data));
|
||||||
str.on('end', err => callback(err, buffer));
|
str.on('end', (err) => callback(err, buffer));
|
||||||
str.on('error', err => callback(err, buffer));
|
str.on('error', (err) => callback(err, buffer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} MessageAttachmentHeaders
|
|
||||||
* @property {string} content-type
|
|
||||||
* @property {string} content-transfer-encoding
|
|
||||||
* @property {string} content-disposition
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} MessageAttachment
|
|
||||||
* @property {string} [name]
|
|
||||||
* @property {string} [type]
|
|
||||||
* @property {string} [charset]
|
|
||||||
* @property {string} [method]
|
|
||||||
* @property {string} [path]
|
|
||||||
* @property {NodeJS.ReadWriteStream} [stream]
|
|
||||||
* @property {boolean} [inline]
|
|
||||||
* @property {MessageAttachment} [alternative]
|
|
||||||
* @property {MessageAttachment[]} [related]
|
|
||||||
* @property {*} [encoded]
|
|
||||||
* @property {*} [data]
|
|
||||||
* @property {MessageAttachmentHeaders} [headers]
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MessageStream extends Stream {
|
class MessageStream extends Stream {
|
||||||
|
readable = true;
|
||||||
|
paused = false;
|
||||||
|
buffer: Buffer | null = Buffer.alloc(MIMECHUNK * 24 * 7);
|
||||||
|
bufferIndex = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Message} message the message to stream
|
* @param {*} message the message to stream
|
||||||
*/
|
*/
|
||||||
constructor(message) {
|
constructor(private message: Message) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Message}
|
* @param {string} [data] the data to output
|
||||||
*/
|
* @param {Function} [callback] the function
|
||||||
this.message = message;
|
* @param {any[]} [args] array of arguments to pass to the callback
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.readable = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.paused = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Buffer}
|
|
||||||
*/
|
|
||||||
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.bufferIndex = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_mixed = () => {
|
const output = (data: string) => {
|
||||||
const boundary = generate_boundary();
|
// can we buffer the data?
|
||||||
output(
|
if (this.buffer != null) {
|
||||||
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
const bytes = Buffer.byteLength(data);
|
||||||
);
|
|
||||||
|
|
||||||
if (this.message.alternative == null) {
|
if (bytes + this.bufferIndex < this.buffer.length) {
|
||||||
output_text(this.message);
|
this.buffer.write(data, this.bufferIndex);
|
||||||
output_message(boundary, this.message.attachments, 0, close);
|
this.bufferIndex += bytes;
|
||||||
} else {
|
}
|
||||||
const cb = () =>
|
// we can't buffer the data, so ship it out!
|
||||||
output_message(boundary, this.message.attachments, 0, close);
|
else if (bytes > this.buffer.length) {
|
||||||
output_alternative(this.message, cb);
|
if (this.bufferIndex) {
|
||||||
}
|
this.emit(
|
||||||
};
|
'data',
|
||||||
|
this.buffer.toString('utf-8', 0, this.bufferIndex)
|
||||||
/**
|
);
|
||||||
* @param {string} boundary the boundary text between outputs
|
this.bufferIndex = 0;
|
||||||
* @param {MessageAttachment[]} list the list of potential messages to output
|
}
|
||||||
* @param {number} index the index of the list item to output
|
|
||||||
* @param {function(): void} callback the function to call if index is greater than upper bound
|
const loops = Math.ceil(data.length / this.buffer.length);
|
||||||
* @returns {void}
|
let loop = 0;
|
||||||
*/
|
while (loop < loops) {
|
||||||
const output_message = (boundary, list, index, callback) => {
|
this.emit(
|
||||||
if (index < list.length) {
|
'data',
|
||||||
output(`--${boundary}${CRLF}`);
|
data.substring(
|
||||||
if (list[index].related) {
|
this.buffer.length * loop,
|
||||||
output_related(list[index], () =>
|
this.buffer.length * (loop + 1)
|
||||||
output_message(boundary, list, index + 1, callback)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
loop++;
|
||||||
output_attachment(list[index], () =>
|
}
|
||||||
output_message(boundary, list, index + 1, callback)
|
} // we need to clean out the buffer, it is getting full
|
||||||
);
|
else {
|
||||||
|
if (!this.paused) {
|
||||||
|
this.emit(
|
||||||
|
'data',
|
||||||
|
this.buffer.toString('utf-8', 0, this.bufferIndex)
|
||||||
|
);
|
||||||
|
this.buffer.write(data, 0);
|
||||||
|
this.bufferIndex = bytes;
|
||||||
|
} else {
|
||||||
|
// we can't empty out the buffer, so let's wait till we resume before adding to it
|
||||||
|
this.once('resume', () => output(data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MessageAttachment} attachment the metadata to use as headers
|
* @param {MessageAttachment | AlternateMessageAttachment} [attachment] the attachment whose headers you would like to output
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_attachment_headers = attachment => {
|
const output_attachment_headers = (
|
||||||
let data = [];
|
attachment: MessageAttachment | AlternateMessageAttachment
|
||||||
const headers = {
|
) => {
|
||||||
|
let data: string[] = [];
|
||||||
|
const headers: Partial<MessageHeaders> = {
|
||||||
'content-type':
|
'content-type':
|
||||||
attachment.type +
|
attachment.type +
|
||||||
(attachment.charset ? `; charset=${attachment.charset}` : '') +
|
(attachment.charset ? `; charset=${attachment.charset}` : '') +
|
||||||
|
@ -339,19 +326,23 @@ class MessageStream extends Stream {
|
||||||
'content-transfer-encoding': 'base64',
|
'content-transfer-encoding': 'base64',
|
||||||
'content-disposition': attachment.inline
|
'content-disposition': attachment.inline
|
||||||
? 'inline'
|
? 'inline'
|
||||||
: `attachment; filename="${mimeWordEncode(attachment.name)}"`,
|
: `attachment; filename="${mimeWordEncode(
|
||||||
|
attachment.name as string
|
||||||
|
)}"`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// allow sender to override default headers
|
// allow sender to override default headers
|
||||||
for (const header in attachment.headers || {}) {
|
if (attachment.headers != null) {
|
||||||
headers[header.toLowerCase()] = attachment.headers[header];
|
for (const header in attachment.headers) {
|
||||||
|
headers[header.toLowerCase()] = attachment.headers[header];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const header in headers) {
|
for (const header in headers) {
|
||||||
data = data.concat([
|
data = data.concat([
|
||||||
fix_header_name_case(header),
|
convertDashDelimitedTextToSnakeCase(header),
|
||||||
': ',
|
': ',
|
||||||
headers[header],
|
headers[header] as string,
|
||||||
CRLF,
|
CRLF,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -360,52 +351,38 @@ class MessageStream extends Stream {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MessageAttachment} attachment the metadata to use as headers
|
* @param {string} data the data to output as base64
|
||||||
* @param {function(): void} callback the function to call after output is finished
|
* @param {function(): void} [callback] the function to call after output is finished
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_attachment = (attachment, callback) => {
|
const output_base64 = (data: string, callback?: () => void) => {
|
||||||
const build = attachment.path
|
const loops = Math.ceil(data.length / MIMECHUNK);
|
||||||
? output_file
|
let loop = 0;
|
||||||
: attachment.stream
|
while (loop < loops) {
|
||||||
? output_stream
|
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
|
||||||
: output_data;
|
loop++;
|
||||||
output_attachment_headers(attachment);
|
}
|
||||||
build(attachment, callback);
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const output_file = (
|
||||||
* @param {MessageAttachment} attachment the metadata to use as headers
|
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||||
* @param {function(): void} callback the function to call after output is finished
|
next: (err: NodeJS.ErrnoException | null) => void
|
||||||
* @returns {void}
|
) => {
|
||||||
*/
|
|
||||||
const output_data = (attachment, callback) => {
|
|
||||||
output_base64(
|
|
||||||
attachment.encoded
|
|
||||||
? attachment.data
|
|
||||||
: Buffer.from(attachment.data).toString('base64'),
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MessageAttachment} attachment the metadata to use as headers
|
|
||||||
* @param {function(NodeJS.ErrnoException): void} next the function to call when the file is closed
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const output_file = (attachment, next) => {
|
|
||||||
const chunk = MIME64CHUNK * 16;
|
const chunk = MIME64CHUNK * 16;
|
||||||
const buffer = Buffer.alloc(chunk);
|
const buffer = Buffer.alloc(chunk);
|
||||||
const closed = fd => fs.closeSync(fd);
|
const closed = (fd: number) => fs.closeSync(fd);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Error} err the error to emit
|
* @param {Error} err the error to emit
|
||||||
* @param {number} fd the file descriptor
|
* @param {number} fd the file descriptor
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const opened = (err, fd) => {
|
const opened = (err: NodeJS.ErrnoException | null, fd: number) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
const read = (err, bytes) => {
|
const read = (err: NodeJS.ErrnoException | null, bytes: number) => {
|
||||||
if (!err && this.readable) {
|
if (!err && this.readable) {
|
||||||
let encoding =
|
let encoding =
|
||||||
attachment && attachment.headers
|
attachment && attachment.headers
|
||||||
|
@ -444,7 +421,7 @@ class MessageStream extends Stream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.open(attachment.path, 'r', opened);
|
fs.open(attachment.path as PathLike, 'r', opened);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -452,20 +429,26 @@ class MessageStream extends Stream {
|
||||||
* @param {function(): void} callback the function to call after output is finished
|
* @param {function(): void} callback the function to call after output is finished
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_stream = (attachment, callback) => {
|
const output_stream = (
|
||||||
if (attachment.stream.readable) {
|
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
|
if (attachment.stream != null && attachment.stream.readable) {
|
||||||
let previous = Buffer.alloc(0);
|
let previous = Buffer.alloc(0);
|
||||||
|
|
||||||
attachment.stream.resume();
|
attachment.stream.resume();
|
||||||
|
|
||||||
attachment.stream.on('end', () => {
|
attachment.stream.on('end', () => {
|
||||||
output_base64(previous.toString('base64'), callback);
|
output_base64(previous.toString('base64'), callback);
|
||||||
this.removeListener('pause', attachment.stream.pause);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
this.removeListener('resume', attachment.stream.resume);
|
this.removeListener('pause', attachment.stream!.pause);
|
||||||
this.removeListener('error', attachment.stream.resume);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
this.removeListener('resume', attachment.stream!.resume);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
this.removeListener('error', attachment.stream!.resume);
|
||||||
});
|
});
|
||||||
|
|
||||||
attachment.stream.on('data', buff => {
|
attachment.stream.on('data', (buff) => {
|
||||||
// do we have bytes from a previous stream data event?
|
// do we have bytes from a previous stream data event?
|
||||||
let buffer = Buffer.isBuffer(buff) ? buff : Buffer.from(buff);
|
let buffer = Buffer.isBuffer(buff) ? buff : Buffer.from(buff);
|
||||||
|
|
||||||
|
@ -492,29 +475,90 @@ class MessageStream extends Stream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const output_attachment = (
|
||||||
|
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
|
const build = attachment.path
|
||||||
|
? output_file
|
||||||
|
: attachment.stream
|
||||||
|
? output_stream
|
||||||
|
: output_data;
|
||||||
|
output_attachment_headers(attachment);
|
||||||
|
build(attachment, callback);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} data the data to output as base64
|
* @param {string} boundary the boundary text between outputs
|
||||||
* @param {function(): void} [callback] the function to call after output is finished
|
* @param {MessageAttachment[]} list the list of potential messages to output
|
||||||
|
* @param {number} index the index of the list item to output
|
||||||
|
* @param {function(): void} callback the function to call if index is greater than upper bound
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_base64 = (data, callback) => {
|
const output_message = (
|
||||||
const loops = Math.ceil(data.length / MIMECHUNK);
|
boundary: string,
|
||||||
let loop = 0;
|
list: MessageAttachment[],
|
||||||
while (loop < loops) {
|
index: number,
|
||||||
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
|
callback: () => void
|
||||||
loop++;
|
) => {
|
||||||
}
|
if (index < list.length) {
|
||||||
if (callback) {
|
output(`--${boundary}${CRLF}`);
|
||||||
|
if (list[index].related) {
|
||||||
|
output_related(list[index], () =>
|
||||||
|
output_message(boundary, list, index + 1, callback)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
output_attachment(list[index], () =>
|
||||||
|
output_message(boundary, list, index + 1, callback)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const output_mixed = () => {
|
||||||
|
const boundary = generate_boundary();
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
output_alternative(
|
||||||
|
// typescript bug; should narrow to { alternative: AlternateMessageAttachment }
|
||||||
|
this.message as Parameters<typeof output_alternative>[0],
|
||||||
|
() => output_message(boundary, this.message.attachments, 0, close)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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_data = (
|
||||||
|
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
|
output_base64(
|
||||||
|
attachment.encoded
|
||||||
|
? attachment.data
|
||||||
|
: Buffer.from(attachment.data).toString('base64'),
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Message} message the message to output
|
* @param {Message} message the message to output
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_text = message => {
|
const output_text = (message: Message) => {
|
||||||
let data = [];
|
let data: string[] = [];
|
||||||
|
|
||||||
data = data.concat([
|
data = data.concat([
|
||||||
'Content-Type:',
|
'Content-Type:',
|
||||||
|
@ -529,12 +573,36 @@ class MessageStream extends Stream {
|
||||||
output(data.join(''));
|
output(data.join(''));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MessageAttachment} message the message to output
|
||||||
|
* @param {function(): void} callback the function to call after output is finished
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const output_related = (
|
||||||
|
message: AlternateMessageAttachment,
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
|
const boundary = generate_boundary();
|
||||||
|
output(
|
||||||
|
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||||
|
);
|
||||||
|
output_attachment(message, () => {
|
||||||
|
output_message(boundary, message.related ?? [], 0, () => {
|
||||||
|
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Message} message the message to output
|
* @param {Message} message the message to output
|
||||||
* @param {function(): void} callback the function to call after output is finished
|
* @param {function(): void} callback the function to call after output is finished
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_alternative = (message, callback) => {
|
const output_alternative = (
|
||||||
|
message: Message & { alternative: AlternateMessageAttachment },
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
const boundary = generate_boundary();
|
const boundary = generate_boundary();
|
||||||
output(
|
output(
|
||||||
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||||
|
@ -557,22 +625,24 @@ class MessageStream extends Stream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const close = (err?: Error) => {
|
||||||
* @param {MessageAttachment} message the message to output
|
if (err) {
|
||||||
* @param {function(): void} callback the function to call after output is finished
|
this.emit('error', err);
|
||||||
* @returns {void}
|
} else {
|
||||||
*/
|
this.emit(
|
||||||
const output_related = (message, callback) => {
|
'data',
|
||||||
const boundary = generate_boundary();
|
this.buffer?.toString('utf-8', 0, this.bufferIndex) ?? ''
|
||||||
output(
|
);
|
||||||
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
this.emit('end');
|
||||||
);
|
}
|
||||||
output_attachment(message, () => {
|
this.buffer = null;
|
||||||
output_message(boundary, message.related, 0, () => {
|
this.bufferIndex = 0;
|
||||||
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
|
this.readable = false;
|
||||||
callback();
|
this.removeAllListeners('resume');
|
||||||
});
|
this.removeAllListeners('pause');
|
||||||
});
|
this.removeAllListeners('error');
|
||||||
|
this.removeAllListeners('data');
|
||||||
|
this.removeAllListeners('end');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -593,18 +663,18 @@ class MessageStream extends Stream {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const output_header = () => {
|
const output_header = () => {
|
||||||
let data = [];
|
let data: string[] = [];
|
||||||
|
|
||||||
for (const header in this.message.header) {
|
for (const header in this.message.header) {
|
||||||
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
|
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
|
||||||
if (
|
if (
|
||||||
!/bcc/i.test(header) &&
|
!/bcc/i.test(header) &&
|
||||||
this.message.header.hasOwnProperty(header)
|
Object.prototype.hasOwnProperty.call(this.message.header, header)
|
||||||
) {
|
) {
|
||||||
data = data.concat([
|
data = data.concat([
|
||||||
fix_header_name_case(header),
|
convertDashDelimitedTextToSnakeCase(header),
|
||||||
': ',
|
': ',
|
||||||
this.message.header[header],
|
this.message.header[header] as string,
|
||||||
CRLF,
|
CRLF,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -614,109 +684,36 @@ class MessageStream extends Stream {
|
||||||
output_header_data();
|
output_header_data();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} data the data to output
|
|
||||||
* @param {function(...args): void} [callback] the function
|
|
||||||
* @param {*[]} [args] array of arguments to pass to the callback
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const output = (data, callback, args) => {
|
|
||||||
const bytes = Buffer.byteLength(data);
|
|
||||||
|
|
||||||
// can we buffer the data?
|
|
||||||
if (bytes + this.bufferIndex < this.buffer.length) {
|
|
||||||
this.buffer.write(data, this.bufferIndex);
|
|
||||||
this.bufferIndex += bytes;
|
|
||||||
if (callback) {
|
|
||||||
callback.apply(null, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// we can't buffer the data, so ship it out!
|
|
||||||
else if (bytes > this.buffer.length) {
|
|
||||||
if (this.bufferIndex) {
|
|
||||||
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
||||||
this.bufferIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loops = Math.ceil(data.length / this.buffer.length);
|
|
||||||
let loop = 0;
|
|
||||||
while (loop < loops) {
|
|
||||||
this.emit(
|
|
||||||
'data',
|
|
||||||
data.substring(
|
|
||||||
this.buffer.length * loop,
|
|
||||||
this.buffer.length * (loop + 1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
loop++;
|
|
||||||
}
|
|
||||||
} // we need to clean out the buffer, it is getting full
|
|
||||||
else {
|
|
||||||
if (!this.paused) {
|
|
||||||
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
||||||
this.buffer.write(data, 0);
|
|
||||||
this.bufferIndex = bytes;
|
|
||||||
// we could get paused after emitting data...
|
|
||||||
if (this.paused) {
|
|
||||||
this.once('resume', () => callback.apply(null, args));
|
|
||||||
} else if (callback) {
|
|
||||||
callback.apply(null, args);
|
|
||||||
}
|
|
||||||
} // we can't empty out the buffer, so let's wait till we resume before adding to it
|
|
||||||
else {
|
|
||||||
this.once('resume', () => output(data, callback, args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {*} [err] the error to emit
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const close = err => {
|
|
||||||
if (err) {
|
|
||||||
this.emit('error', err);
|
|
||||||
} else {
|
|
||||||
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
|
|
||||||
this.emit('end');
|
|
||||||
}
|
|
||||||
this.buffer = null;
|
|
||||||
this.bufferIndex = 0;
|
|
||||||
this.readable = false;
|
|
||||||
this.removeAllListeners('resume');
|
|
||||||
this.removeAllListeners('pause');
|
|
||||||
this.removeAllListeners('error');
|
|
||||||
this.removeAllListeners('data');
|
|
||||||
this.removeAllListeners('end');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.once('destroy', close);
|
this.once('destroy', close);
|
||||||
process.nextTick(output_header);
|
process.nextTick(output_header);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* pause the stream
|
* pause the stream
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
pause() {
|
public pause() {
|
||||||
this.paused = true;
|
this.paused = true;
|
||||||
this.emit('pause');
|
this.emit('pause');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* resume the stream
|
* resume the stream
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
resume() {
|
public resume() {
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
this.emit('resume');
|
this.emit('resume');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* destroy the stream
|
* destroy the stream
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
destroy() {
|
public destroy() {
|
||||||
this.emit(
|
this.emit(
|
||||||
'destroy',
|
'destroy',
|
||||||
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
|
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
|
||||||
|
@ -724,14 +721,11 @@ class MessageStream extends Stream {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* destroy the stream at first opportunity
|
* destroy the stream at first opportunity
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
destroySoon() {
|
public destroySoon() {
|
||||||
this.emit('destroy');
|
this.emit('destroy');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.Message = Message;
|
|
||||||
exports.BUFFERSIZE = BUFFERSIZE;
|
|
||||||
exports.create = headers => new Message(headers);
|
|
139
smtp/response.js
139
smtp/response.js
|
@ -1,139 +0,0 @@
|
||||||
const SMTPError = require('./error');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('net').Socket} Socket
|
|
||||||
* @typedef {import('tls').TLSSocket} TLSSocket
|
|
||||||
*/
|
|
||||||
|
|
||||||
class SMTPResponse {
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {Socket | TLSSocket} stream the open socket to stream a response from
|
|
||||||
* @param {number} timeout the time to wait (in milliseconds) before closing the socket
|
|
||||||
* @param {function(Error): void} onerror the function to call on error
|
|
||||||
*/
|
|
||||||
constructor(stream, timeout, onerror) {
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const notify = () => {
|
|
||||||
if (buffer.length) {
|
|
||||||
// parse buffer for response codes
|
|
||||||
const line = buffer.replace('\r', '');
|
|
||||||
if (
|
|
||||||
!line
|
|
||||||
.trim()
|
|
||||||
.split(/\n/)
|
|
||||||
.pop()
|
|
||||||
.match(/^(\d{3})\s/)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = line ? line.match(/(\d+)\s?(.*)/) : null;
|
|
||||||
const data =
|
|
||||||
match !== null
|
|
||||||
? { code: match[1], message: match[2], data: line }
|
|
||||||
: { code: -1, data: line };
|
|
||||||
|
|
||||||
stream.emit('response', null, data);
|
|
||||||
buffer = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Error} err the error object
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const error = err => {
|
|
||||||
stream.emit(
|
|
||||||
'response',
|
|
||||||
SMTPError('connection encountered an error', SMTPError.ERROR, err)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Error} err the error object
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const timedout = err => {
|
|
||||||
stream.end();
|
|
||||||
stream.emit(
|
|
||||||
'response',
|
|
||||||
SMTPError(
|
|
||||||
'timedout while connecting to smtp server',
|
|
||||||
SMTPError.TIMEDOUT,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string | Buffer} data the data
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const watch = data => {
|
|
||||||
if (data !== null) {
|
|
||||||
buffer += data.toString();
|
|
||||||
notify();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Error} err the error object
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const close = err => {
|
|
||||||
stream.emit(
|
|
||||||
'response',
|
|
||||||
SMTPError('connection has closed', SMTPError.CONNECTIONCLOSED, err)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Error} err the error object
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const end = err => {
|
|
||||||
stream.emit(
|
|
||||||
'response',
|
|
||||||
SMTPError('connection has ended', SMTPError.CONNECTIONENDED, err)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Error} [err] the error object
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
this.stop = err => {
|
|
||||||
stream.removeAllListeners('response');
|
|
||||||
stream.removeListener('data', watch);
|
|
||||||
stream.removeListener('end', end);
|
|
||||||
stream.removeListener('close', close);
|
|
||||||
stream.removeListener('error', error);
|
|
||||||
|
|
||||||
if (err != null && typeof onerror === 'function') {
|
|
||||||
onerror(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
stream.on('data', watch);
|
|
||||||
stream.on('end', end);
|
|
||||||
stream.on('close', close);
|
|
||||||
stream.on('error', error);
|
|
||||||
stream.setTimeout(timeout, timedout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.SMTPResponse = SMTPResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Socket | TLSSocket} stream the open socket to stream a response from
|
|
||||||
* @param {number} timeout the time to wait (in milliseconds) before closing the socket
|
|
||||||
* @param {function(Error): void} onerror the function to call on error
|
|
||||||
* @returns {SMTPResponse} the smtp response
|
|
||||||
*/
|
|
||||||
exports.monitor = (stream, timeout, onerror) =>
|
|
||||||
new SMTPResponse(stream, timeout, onerror);
|
|
112
smtp/response.ts
Normal file
112
smtp/response.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { makeSMTPError, SMTPErrorStates } from './error';
|
||||||
|
import type { Socket } from 'net';
|
||||||
|
import type { TLSSocket } from 'tls';
|
||||||
|
|
||||||
|
export class SMTPResponse {
|
||||||
|
public readonly stop: (err?: Error) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
stream: Socket | TLSSocket,
|
||||||
|
timeout: number,
|
||||||
|
onerror: (err: Error) => void
|
||||||
|
) {
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
const notify = () => {
|
||||||
|
if (buffer.length) {
|
||||||
|
// parse buffer for response codes
|
||||||
|
const line = buffer.replace('\r', '');
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
line
|
||||||
|
.trim()
|
||||||
|
.split(/\n/)
|
||||||
|
.pop()
|
||||||
|
?.match(/^(\d{3})\s/) ?? false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = line ? line.match(/(\d+)\s?(.*)/) : null;
|
||||||
|
const data =
|
||||||
|
match !== null
|
||||||
|
? { code: match[1], message: match[2], data: line }
|
||||||
|
: { code: -1, data: line };
|
||||||
|
|
||||||
|
stream.emit('response', null, data);
|
||||||
|
buffer = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (err: Error) => {
|
||||||
|
stream.emit(
|
||||||
|
'response',
|
||||||
|
makeSMTPError(
|
||||||
|
'connection encountered an error',
|
||||||
|
SMTPErrorStates.ERROR,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timedout = (err?: Error) => {
|
||||||
|
stream.end();
|
||||||
|
stream.emit(
|
||||||
|
'response',
|
||||||
|
makeSMTPError(
|
||||||
|
'timedout while connecting to smtp server',
|
||||||
|
SMTPErrorStates.TIMEDOUT,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const watch = (data: string | Buffer) => {
|
||||||
|
if (data !== null) {
|
||||||
|
buffer += data.toString();
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = (err: Error) => {
|
||||||
|
stream.emit(
|
||||||
|
'response',
|
||||||
|
makeSMTPError(
|
||||||
|
'connection has closed',
|
||||||
|
SMTPErrorStates.CONNECTIONCLOSED,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = (err: Error) => {
|
||||||
|
stream.emit(
|
||||||
|
'response',
|
||||||
|
makeSMTPError(
|
||||||
|
'connection has ended',
|
||||||
|
SMTPErrorStates.CONNECTIONENDED,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stop = (err) => {
|
||||||
|
stream.removeAllListeners('response');
|
||||||
|
stream.removeListener('data', watch);
|
||||||
|
stream.removeListener('end', end);
|
||||||
|
stream.removeListener('close', close);
|
||||||
|
stream.removeListener('error', error);
|
||||||
|
|
||||||
|
if (err != null && typeof onerror === 'function') {
|
||||||
|
onerror(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.on('data', watch);
|
||||||
|
stream.on('end', end);
|
||||||
|
stream.on('close', close);
|
||||||
|
stream.on('error', error);
|
||||||
|
stream.setTimeout(timeout, timedout);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,75 +1,49 @@
|
||||||
const { Socket } = require('net');
|
import { Socket } from 'net';
|
||||||
const { createHmac } = require('crypto');
|
import { createHmac } from 'crypto';
|
||||||
const { hostname } = require('os');
|
import { hostname } from 'os';
|
||||||
const { connect, createSecureContext, TLSSocket } = require('tls');
|
import { connect, createSecureContext, TLSSocket } from 'tls';
|
||||||
const { EventEmitter } = require('events');
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
const SMTPResponse = require('./response');
|
import { SMTPResponse } from './response';
|
||||||
const SMTPError = require('./error');
|
import { makeSMTPError, SMTPErrorStates } from './error';
|
||||||
|
|
||||||
/**
|
|
||||||
* @readonly
|
|
||||||
* @type {5000}
|
|
||||||
*/
|
|
||||||
const TIMEOUT = 5000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @readonly
|
|
||||||
* @type {25}
|
|
||||||
*/
|
|
||||||
const SMTP_PORT = 25;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @readonly
|
|
||||||
* @type {465}
|
|
||||||
*/
|
|
||||||
const SMTP_SSL_PORT = 465;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @readonly
|
|
||||||
* @type {587}
|
|
||||||
*/
|
|
||||||
const SMTP_TLS_PORT = 587;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @readonly
|
|
||||||
* @type {'\r\n'}
|
|
||||||
*/
|
|
||||||
const CRLF = '\r\n';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @readonly
|
* @readonly
|
||||||
* @enum
|
* @enum
|
||||||
*/
|
*/
|
||||||
const AUTH_METHODS = {
|
export const AUTH_METHODS = {
|
||||||
PLAIN: /** @type {'PLAIN'} */ ('PLAIN'),
|
PLAIN: 'PLAIN',
|
||||||
CRAM_MD5: /** @type {'CRAM-MD5'} */ ('CRAM-MD5'),
|
'CRAM-MD5': 'CRAM-MD5',
|
||||||
LOGIN: /** @type {'LOGIN'} */ ('LOGIN'),
|
LOGIN: 'LOGIN',
|
||||||
XOAUTH2: /** @type {'XOAUTH2'} */ ('XOAUTH2'),
|
XOAUTH2: 'XOAUTH2',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @readonly
|
* @readonly
|
||||||
* @enum
|
* @enum
|
||||||
*/
|
*/
|
||||||
const SMTPState = {
|
export const SMTPState = {
|
||||||
NOTCONNECTED: /** @type {0} */ (0),
|
NOTCONNECTED: 0,
|
||||||
CONNECTING: /** @type {1} */ (1),
|
CONNECTING: 1,
|
||||||
CONNECTED: /** @type {2} */ (2),
|
CONNECTED: 2,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
/**
|
export const DEFAULT_TIMEOUT = 5000 as const;
|
||||||
* @type {0 | 1}
|
|
||||||
*/
|
const SMTP_PORT = 25 as const;
|
||||||
let DEBUG = 0;
|
const SMTP_SSL_PORT = 465 as const;
|
||||||
|
const SMTP_TLS_PORT = 587 as const;
|
||||||
|
const CRLF = '\r\n' 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}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const log = (...args) => {
|
const log = (...args: any[]) => {
|
||||||
if (DEBUG === 1) {
|
if (DEBUG === 1) {
|
||||||
args.forEach(d =>
|
args.forEach((d) =>
|
||||||
console.log(
|
console.log(
|
||||||
typeof d === 'object'
|
typeof d === 'object'
|
||||||
? d instanceof Error
|
? d instanceof Error
|
||||||
|
@ -86,35 +60,63 @@ const log = (...args) => {
|
||||||
* @param {...*} args the arguments to apply to the function
|
* @param {...*} args the arguments to apply to the function
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const caller = (callback, ...args) => {
|
const caller = (callback?: (...rest: any[]) => void, ...args: any[]) => {
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback.apply(null, args);
|
callback(...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SMTP extends EventEmitter {
|
export interface SMTPSocketOptions {
|
||||||
|
key: string;
|
||||||
|
ca: string;
|
||||||
|
cert: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SMTPConnectionOptions {
|
||||||
|
timeout: number | null;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
domain: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
ssl: boolean | SMTPSocketOptions;
|
||||||
|
tls: boolean | SMTPSocketOptions;
|
||||||
|
authentication: (keyof typeof AUTH_METHODS)[];
|
||||||
|
logger: (...args: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectOptions {
|
||||||
|
ssl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SMTPConnection extends EventEmitter {
|
||||||
|
public readonly user: () => string;
|
||||||
|
public readonly password: () => string;
|
||||||
|
public readonly timeout: number = DEFAULT_TIMEOUT;
|
||||||
|
|
||||||
|
protected readonly log = log;
|
||||||
|
protected readonly authentication: (keyof typeof AUTH_METHODS)[] = [
|
||||||
|
AUTH_METHODS['CRAM-MD5'],
|
||||||
|
AUTH_METHODS.LOGIN,
|
||||||
|
AUTH_METHODS.PLAIN,
|
||||||
|
AUTH_METHODS.XOAUTH2,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected _state: 0 | 1 | 2 = SMTPState.NOTCONNECTED;
|
||||||
|
protected _secure = false;
|
||||||
|
protected loggedin = false;
|
||||||
|
|
||||||
|
protected sock: Socket | TLSSocket | null = null;
|
||||||
|
protected features: { [index: string]: string | boolean } | null = null;
|
||||||
|
protected monitor: SMTPResponse | null = null;
|
||||||
|
protected domain = hostname();
|
||||||
|
protected host = 'localhost';
|
||||||
|
protected ssl: boolean | SMTPSocketOptions = false;
|
||||||
|
protected tls: boolean | SMTPSocketOptions = false;
|
||||||
|
protected port: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP class written using python's (2.7) smtplib.py as a base
|
* SMTP class written using python's (2.7) smtplib.py as a base
|
||||||
*
|
|
||||||
* @typedef {Object} SMTPSocketOptions
|
|
||||||
* @property {string} key
|
|
||||||
* @property {string} ca
|
|
||||||
* @property {string} cert
|
|
||||||
*
|
|
||||||
* @typedef {Object} SMTPOptions
|
|
||||||
* @property {number} [timeout]
|
|
||||||
* @property {string} [user]
|
|
||||||
* @property {string} [password]
|
|
||||||
* @property {string} [domain]
|
|
||||||
* @property {string} [host]
|
|
||||||
* @property {number} [port]
|
|
||||||
* @property {boolean|SMTPSocketOptions} [ssl]
|
|
||||||
* @property {boolean|SMTPSocketOptions} [tls]
|
|
||||||
* @property {string[]} [authentication]
|
|
||||||
* @property {function(...any): void} [logger]
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {SMTPOptions} [options] instance options
|
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
timeout,
|
timeout,
|
||||||
|
@ -127,146 +129,98 @@ class SMTP extends EventEmitter {
|
||||||
tls,
|
tls,
|
||||||
logger,
|
logger,
|
||||||
authentication,
|
authentication,
|
||||||
} = {}) {
|
}: Partial<SMTPConnectionOptions> = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
/**
|
if (Array.isArray(authentication)) {
|
||||||
* @private
|
this.authentication = authentication;
|
||||||
* @type {0 | 1 | 2}
|
}
|
||||||
*/
|
|
||||||
this._state = SMTPState.NOTCONNECTED;
|
|
||||||
|
|
||||||
/**
|
if (typeof timeout === 'number') {
|
||||||
* @private
|
this.timeout = timeout;
|
||||||
* @type {boolean}
|
}
|
||||||
*/
|
|
||||||
this._secure = false;
|
|
||||||
|
|
||||||
/**
|
if (typeof domain === 'string') {
|
||||||
* @type {Socket|TLSSocket}
|
this.domain = domain;
|
||||||
*/
|
}
|
||||||
this.sock = null;
|
|
||||||
|
|
||||||
/**
|
if (typeof host === 'string') {
|
||||||
* @type {{ [i: string]: string | boolean }}
|
this.host = host;
|
||||||
*/
|
}
|
||||||
this.features = null;
|
|
||||||
|
|
||||||
/**
|
if (
|
||||||
* @type {SMTPResponse.SMTPResponse}
|
|
||||||
*/
|
|
||||||
this.monitor = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {string[]}
|
|
||||||
*/
|
|
||||||
this.authentication = Array.isArray(authentication)
|
|
||||||
? authentication
|
|
||||||
: [
|
|
||||||
AUTH_METHODS.CRAM_MD5,
|
|
||||||
AUTH_METHODS.LOGIN,
|
|
||||||
AUTH_METHODS.PLAIN,
|
|
||||||
AUTH_METHODS.XOAUTH2,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number} }
|
|
||||||
*/
|
|
||||||
this.timeout = typeof timeout === 'number' ? timeout : TIMEOUT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {string} }
|
|
||||||
*/
|
|
||||||
this.domain = typeof domain === 'string' ? domain : hostname();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {string} }
|
|
||||||
*/
|
|
||||||
this.host = typeof host === 'string' ? host : 'localhost';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean|SMTPSocketOptions}
|
|
||||||
*/
|
|
||||||
this.ssl =
|
|
||||||
ssl != null &&
|
ssl != null &&
|
||||||
(typeof ssl === 'boolean' ||
|
(typeof ssl === 'boolean' ||
|
||||||
(typeof ssl === 'object' && Array.isArray(ssl) === false))
|
(typeof ssl === 'object' && Array.isArray(ssl) === false))
|
||||||
? ssl
|
) {
|
||||||
: false;
|
this.ssl = ssl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
if (
|
||||||
* @type {boolean|SMTPSocketOptions}
|
|
||||||
*/
|
|
||||||
this.tls =
|
|
||||||
tls != null &&
|
tls != null &&
|
||||||
(typeof tls === 'boolean' ||
|
(typeof tls === 'boolean' ||
|
||||||
(typeof tls === 'object' && Array.isArray(tls) === false))
|
(typeof tls === 'object' && Array.isArray(tls) === false))
|
||||||
? tls
|
) {
|
||||||
: false;
|
this.tls = tls;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.port = port || (ssl ? SMTP_SSL_PORT : tls ? SMTP_TLS_PORT : SMTP_PORT);
|
this.port = port || (ssl ? SMTP_SSL_PORT : tls ? SMTP_TLS_PORT : SMTP_PORT);
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.loggedin = user && password ? false : true;
|
this.loggedin = user && password ? false : true;
|
||||||
|
|
||||||
// keep these strings hidden when quicky debugging/logging
|
// keep these strings hidden when quicky debugging/logging
|
||||||
this.user = /** @returns {string} */ () => user;
|
this.user = () => user as string;
|
||||||
this.password = /** @returns {string} */ () => password;
|
this.password = () => password as string;
|
||||||
|
|
||||||
this.log = typeof logger === 'function' ? logger : log;
|
if (typeof logger === 'function') {
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {0 | 1} level -
|
* @param {0 | 1} level -
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
debug(level) {
|
public debug(level: 0 | 1) {
|
||||||
DEBUG = level;
|
DEBUG = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {number} the current state
|
* @public
|
||||||
|
* @returns {SMTPState} the current state
|
||||||
*/
|
*/
|
||||||
state() {
|
public state() {
|
||||||
return this._state;
|
return this._state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @returns {boolean} whether or not the instance is authorized
|
* @returns {boolean} whether or not the instance is authorized
|
||||||
*/
|
*/
|
||||||
authorized() {
|
public authorized() {
|
||||||
return this.loggedin;
|
return this.loggedin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ConnectOptions
|
* @public
|
||||||
* @property {boolean} [ssl]
|
|
||||||
*
|
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {number} [port] the port to use for the connection
|
* @param {number} [port] the port to use for the connection
|
||||||
* @param {string} [host] the hostname to use for the connection
|
* @param {string} [host] the hostname to use for the connection
|
||||||
* @param {ConnectOptions} [options={}] the options
|
* @param {ConnectOptions} [options={}] the options
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
connect(callback, port = this.port, host = this.host, options = {}) {
|
public connect(
|
||||||
|
callback: (...rest: any[]) => void,
|
||||||
|
port: number = this.port,
|
||||||
|
host: string = this.host,
|
||||||
|
options: ConnectOptions = {}
|
||||||
|
) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.ssl = options.ssl || this.ssl;
|
this.ssl = options.ssl || this.ssl;
|
||||||
|
|
||||||
if (this._state !== SMTPState.NOTCONNECTED) {
|
if (this._state !== SMTPState.NOTCONNECTED) {
|
||||||
this.quit(() =>
|
this.quit(() => this.connect(callback, port, host, options));
|
||||||
this.connect(
|
|
||||||
callback,
|
|
||||||
port,
|
|
||||||
host,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -285,9 +239,9 @@ class SMTP extends EventEmitter {
|
||||||
this.close(true);
|
this.close(true);
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError(
|
makeSMTPError(
|
||||||
'could not establish an ssl connection',
|
'could not establish an ssl connection',
|
||||||
SMTPError.CONNECTIONAUTH
|
SMTPErrorStates.CONNECTIONAUTH
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -300,7 +254,7 @@ class SMTP extends EventEmitter {
|
||||||
* @param {Error} err err
|
* @param {Error} err err
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const connectedErrBack = err => {
|
const connectedErrBack = (err?: Error) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
connected();
|
connected();
|
||||||
} else {
|
} else {
|
||||||
|
@ -308,12 +262,19 @@ class SMTP extends EventEmitter {
|
||||||
this.log(err);
|
this.log(err);
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError('could not connect', SMTPError.COULDNOTCONNECT, err)
|
makeSMTPError(
|
||||||
|
'could not connect',
|
||||||
|
SMTPErrorStates.COULDNOTCONNECT,
|
||||||
|
err
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = (err, msg) => {
|
const response = (
|
||||||
|
err: Error | null | undefined,
|
||||||
|
msg: { code: string | number; data: string }
|
||||||
|
) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
|
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
|
||||||
return;
|
return;
|
||||||
|
@ -331,9 +292,9 @@ class SMTP extends EventEmitter {
|
||||||
this.quit(() => {
|
this.quit(() => {
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError(
|
makeSMTPError(
|
||||||
'bad response on connection',
|
'bad response on connection',
|
||||||
SMTPError.BADRESPONSE,
|
SMTPErrorStates.BADRESPONSE,
|
||||||
err,
|
err,
|
||||||
msg.data
|
msg.data
|
||||||
)
|
)
|
||||||
|
@ -354,14 +315,10 @@ class SMTP extends EventEmitter {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.sock = new Socket();
|
this.sock = new Socket();
|
||||||
this.sock.connect(
|
this.sock.connect(this.port, this.host, connectedErrBack);
|
||||||
this.port,
|
|
||||||
this.host,
|
|
||||||
connectedErrBack
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.monitor = SMTPResponse.monitor(this.sock, this.timeout, () =>
|
this.monitor = new SMTPResponse(this.sock, this.timeout, () =>
|
||||||
this.close(true)
|
this.close(true)
|
||||||
);
|
);
|
||||||
this.sock.once('response', response);
|
this.sock.once('response', response);
|
||||||
|
@ -369,11 +326,12 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {string} str the string to send
|
* @param {string} str the string to send
|
||||||
* @param {*} callback function to call after response
|
* @param {*} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
send(str, callback) {
|
public send(str: string, callback: (...args: any[]) => void) {
|
||||||
if (this.sock && this._state === SMTPState.CONNECTED) {
|
if (this.sock && this._state === SMTPState.CONNECTED) {
|
||||||
this.log(str);
|
this.log(str);
|
||||||
|
|
||||||
|
@ -390,25 +348,36 @@ class SMTP extends EventEmitter {
|
||||||
this.close(true);
|
this.close(true);
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError('no connection has been established', SMTPError.NOCONNECTION)
|
makeSMTPError(
|
||||||
|
'no connection has been established',
|
||||||
|
SMTPErrorStates.NOCONNECTION
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {string} cmd command to issue
|
* @param {string} cmd command to issue
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {(number[] | number)} [codes=[250]] array codes
|
* @param {(number[] | number)} [codes=[250]] array codes
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
command(cmd, callback, codes = [250]) {
|
public command(
|
||||||
|
cmd: string,
|
||||||
|
callback: (...rest: any[]) => void,
|
||||||
|
codes: number[] | number = [250]
|
||||||
|
) {
|
||||||
const codesArray = Array.isArray(codes)
|
const codesArray = Array.isArray(codes)
|
||||||
? codes
|
? codes
|
||||||
: typeof codes === 'number'
|
: typeof codes === 'number'
|
||||||
? [codes]
|
? [codes]
|
||||||
: [250];
|
: [250];
|
||||||
|
|
||||||
const response = (err, msg) => {
|
const response = (
|
||||||
|
err: Error | null | undefined,
|
||||||
|
msg: { code: string | number; data: string; message: string }
|
||||||
|
) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
} else {
|
} else {
|
||||||
|
@ -421,7 +390,12 @@ class SMTP extends EventEmitter {
|
||||||
}'${suffix}`;
|
}'${suffix}`;
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError(errorMessage, SMTPError.BADRESPONSE, null, msg.data)
|
makeSMTPError(
|
||||||
|
errorMessage,
|
||||||
|
SMTPErrorStates.BADRESPONSE,
|
||||||
|
null,
|
||||||
|
msg.data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,7 +405,8 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP 'helo' command.
|
* @public
|
||||||
|
* @description SMTP 'helo' command.
|
||||||
*
|
*
|
||||||
* Hostname to send for self command defaults to the FQDN of the local
|
* Hostname to send for self command defaults to the FQDN of the local
|
||||||
* host.
|
* host.
|
||||||
|
@ -440,7 +415,7 @@ class SMTP extends EventEmitter {
|
||||||
* @param {string} domain the domain to associate with the 'helo' request
|
* @param {string} domain the domain to associate with the 'helo' request
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
helo(callback, domain) {
|
public helo(callback: (...rest: any[]) => void, domain?: string) {
|
||||||
this.command(`helo ${domain || this.domain}`, (err, data) => {
|
this.command(`helo ${domain || this.domain}`, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
|
@ -452,11 +427,16 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
starttls(callback) {
|
public starttls(callback: (...rest: any[]) => void) {
|
||||||
const response = (err, msg) => {
|
const response = (err: Error, msg: { data: unknown }) => {
|
||||||
|
if (this.sock == null) {
|
||||||
|
throw new Error('null socket');
|
||||||
|
}
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message += ' while establishing a starttls session';
|
err.message += ' while establishing a starttls session';
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
|
@ -466,7 +446,7 @@ class SMTP extends EventEmitter {
|
||||||
);
|
);
|
||||||
const secureSocket = new TLSSocket(this.sock, { secureContext });
|
const secureSocket = new TLSSocket(this.sock, { secureContext });
|
||||||
|
|
||||||
secureSocket.on('error', err => {
|
secureSocket.on('error', (err: Error) => {
|
||||||
this.close(true);
|
this.close(true);
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
});
|
});
|
||||||
|
@ -474,7 +454,7 @@ class SMTP extends EventEmitter {
|
||||||
this._secure = true;
|
this._secure = true;
|
||||||
this.sock = secureSocket;
|
this.sock = secureSocket;
|
||||||
|
|
||||||
SMTPResponse.monitor(this.sock, this.timeout, () => this.close(true));
|
new SMTPResponse(this.sock, this.timeout, () => this.close(true));
|
||||||
caller(callback, msg.data);
|
caller(callback, msg.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -483,15 +463,16 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {string} data the string to parse for features
|
* @param {string} data the string to parse for features
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
parse_smtp_features(data) {
|
public parse_smtp_features(data: string) {
|
||||||
// According to RFC1869 some (badly written)
|
// According to RFC1869 some (badly written)
|
||||||
// MTA's will disconnect on an ehlo. Toss an exception if
|
// MTA's will disconnect on an ehlo. Toss an exception if
|
||||||
// that happens -ddm
|
// that happens -ddm
|
||||||
|
|
||||||
data.split('\n').forEach(ext => {
|
data.split('\n').forEach((ext) => {
|
||||||
const parse = ext.match(/^(?:\d+[-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
|
const parse = ext.match(/^(?:\d+[-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
|
||||||
|
|
||||||
// To be able to communicate with as many SMTP servers as possible,
|
// To be able to communicate with as many SMTP servers as possible,
|
||||||
|
@ -501,7 +482,7 @@ class SMTP extends EventEmitter {
|
||||||
// 2) There are some servers that only advertise the auth methods we
|
// 2) There are some servers that only advertise the auth methods we
|
||||||
// support using the old style.
|
// support using the old style.
|
||||||
|
|
||||||
if (parse != null) {
|
if (parse != null && this.features != null) {
|
||||||
// RFC 1869 requires a space between ehlo keyword and parameters.
|
// RFC 1869 requires a space between ehlo keyword and parameters.
|
||||||
// It's actually stricter, in that only spaces are allowed between
|
// It's actually stricter, in that only spaces are allowed between
|
||||||
// parameters, but were not going to check for that here. Note
|
// parameters, but were not going to check for that here. Note
|
||||||
|
@ -512,11 +493,12 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {string} domain the domain to associate with the 'ehlo' request
|
* @param {string} domain the domain to associate with the 'ehlo' request
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
ehlo(callback, domain) {
|
public ehlo(callback: (...rest: any[]) => void, domain?: string) {
|
||||||
this.features = {};
|
this.features = {};
|
||||||
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
|
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -534,106 +516,116 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {string} opt the features keyname to check
|
* @param {string} opt the features keyname to check
|
||||||
* @returns {boolean} whether the extension exists
|
* @returns {boolean} whether the extension exists
|
||||||
*/
|
*/
|
||||||
has_extn(opt) {
|
public has_extn(opt: string): boolean {
|
||||||
return this.features[opt.toLowerCase()] === undefined;
|
return (this.features ?? {})[opt.toLowerCase()] === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP 'help' command, returns text from the server
|
* @public
|
||||||
|
* @description SMTP 'help' command, returns text from the server
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {string} domain the domain to associate with the 'help' request
|
* @param {string} domain the domain to associate with the 'help' request
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
help(callback, domain) {
|
public help(callback: (...rest: any[]) => void, domain: string) {
|
||||||
this.command(domain ? `help ${domain}` : 'help', callback, [211, 214]);
|
this.command(domain ? `help ${domain}` : 'help', callback, [211, 214]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
rset(callback) {
|
public rset(callback: (...rest: any[]) => void) {
|
||||||
this.command('rset', callback);
|
this.command('rset', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
noop(callback) {
|
public noop(callback: (...rest: any[]) => void) {
|
||||||
this.send('noop', callback);
|
this.send('noop', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {string} from the sender
|
* @param {string} from the sender
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
mail(callback, from) {
|
public mail(callback: (...rest: any[]) => void, from: string) {
|
||||||
this.command(`mail FROM:${from}`, callback);
|
this.command(`mail FROM:${from}`, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @param {string} to the receiver
|
* @param {string} to the receiver
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
rcpt(callback, to) {
|
public rcpt(callback: (...rest: any[]) => void, to: string) {
|
||||||
this.command(`RCPT TO:${to}`, callback, [250, 251]);
|
this.command(`RCPT TO:${to}`, callback, [250, 251]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
data(callback) {
|
public data(callback: (...rest: any[]) => void) {
|
||||||
this.command('data', callback, [354]);
|
this.command('data', callback, [354]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
data_end(callback) {
|
public data_end(callback: (...rest: any[]) => void) {
|
||||||
this.command(`${CRLF}.`, callback);
|
this.command(`${CRLF}.`, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {string} data the message to send
|
* @param {string} data the message to send
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
message(data) {
|
public message(data: string) {
|
||||||
this.log(data);
|
this.log(data);
|
||||||
this.sock.write(data);
|
this.sock?.write(data) ?? this.log('no socket to write to');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP 'verify' command -- checks for address validity.
|
* @public
|
||||||
*
|
* @description SMTP 'verify' command -- checks for address validity.
|
||||||
* @param {string} address the address to validate
|
* @param {string} address the address to validate
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
verify(address, callback) {
|
public verify(address: string, callback: (...rest: any[]) => void) {
|
||||||
this.command(`vrfy ${address}`, callback, [250, 251, 252]);
|
this.command(`vrfy ${address}`, callback, [250, 251, 252]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP 'expn' command -- expands a mailing list.
|
* @public
|
||||||
*
|
* @description SMTP 'expn' command -- expands a mailing list.
|
||||||
* @param {string} address the mailing list to expand
|
* @param {string} address the mailing list to expand
|
||||||
* @param {function(...*): void} callback function to call after response
|
* @param {function(...*): void} callback function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
expn(address, callback) {
|
public expn(address: string, callback: (...rest: any[]) => void) {
|
||||||
this.command(`expn ${address}`, callback);
|
this.command(`expn ${address}`, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls this.ehlo() and, if an error occurs, this.helo().
|
* @public
|
||||||
|
* @description Calls this.ehlo() and, if an error occurs, this.helo().
|
||||||
*
|
*
|
||||||
* If there has been no previous EHLO or HELO command self session, self
|
* If there has been no previous EHLO or HELO command self session, self
|
||||||
* method tries ESMTP EHLO first.
|
* method tries ESMTP EHLO first.
|
||||||
|
@ -642,10 +634,14 @@ class SMTP extends EventEmitter {
|
||||||
* @param {string} [domain] the domain to associate with the command
|
* @param {string} [domain] the domain to associate with the command
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
ehlo_or_helo_if_needed(callback, domain) {
|
public ehlo_or_helo_if_needed(
|
||||||
|
callback: (...rest: any[]) => void,
|
||||||
|
domain?: string
|
||||||
|
) {
|
||||||
// is this code callable...?
|
// is this code callable...?
|
||||||
if (!this.features) {
|
if (!this.features) {
|
||||||
const response = (err, data) => caller(callback, err, data);
|
const response = (err: Error, data: unknown) =>
|
||||||
|
caller(callback, err, data);
|
||||||
this.ehlo((err, data) => {
|
this.ehlo((err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.helo(response, domain);
|
this.helo(response, domain);
|
||||||
|
@ -657,6 +653,8 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
|
*
|
||||||
* Log in on an SMTP server that requires authentication.
|
* Log in on an SMTP server that requires authentication.
|
||||||
*
|
*
|
||||||
* If there has been no previous EHLO or HELO command self session, self
|
* If there has been no previous EHLO or HELO command self session, self
|
||||||
|
@ -670,28 +668,33 @@ class SMTP extends EventEmitter {
|
||||||
* @param {{ method: string, domain: string }} [options] login options
|
* @param {{ method: string, domain: string }} [options] login options
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
login(callback, user, password, options) {
|
public login(
|
||||||
|
callback: (...rest: any[]) => void,
|
||||||
|
user?: string,
|
||||||
|
password?: string,
|
||||||
|
options: { method?: string; domain?: string } = {}
|
||||||
|
) {
|
||||||
const login = {
|
const login = {
|
||||||
user: user ? () => user : this.user,
|
user: user ? () => user : this.user,
|
||||||
password: password ? () => password : this.password,
|
password: password ? () => password : this.password,
|
||||||
method: options && options.method ? options.method.toUpperCase() : '',
|
method: options?.method?.toUpperCase() ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const domain = options && options.domain ? options.domain : this.domain;
|
const domain = options?.domain || this.domain;
|
||||||
|
|
||||||
const initiate = (err, data) => {
|
const initiate = (err: Error | null | undefined, data: unknown) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let method = null;
|
let method: keyof typeof AUTH_METHODS | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} challenge challenge
|
* @param {string} challenge challenge
|
||||||
* @returns {string} base64 cram hash
|
* @returns {string} base64 cram hash
|
||||||
*/
|
*/
|
||||||
const encode_cram_md5 = challenge => {
|
const encode_cram_md5 = (challenge: string): string => {
|
||||||
const hmac = createHmac('md5', login.password());
|
const hmac = createHmac('md5', login.password());
|
||||||
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
|
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
|
||||||
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
|
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
|
||||||
|
@ -702,7 +705,7 @@ class SMTP extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @returns {string} base64 login/password
|
* @returns {string} base64 login/password
|
||||||
*/
|
*/
|
||||||
const encode_plain = () =>
|
const encode_plain = (): string =>
|
||||||
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
|
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
|
||||||
'base64'
|
'base64'
|
||||||
);
|
);
|
||||||
|
@ -711,7 +714,7 @@ class SMTP extends EventEmitter {
|
||||||
* @see https://developers.google.com/gmail/xoauth2_protocol
|
* @see https://developers.google.com/gmail/xoauth2_protocol
|
||||||
* @returns {string} base64 xoauth2 auth token
|
* @returns {string} base64 xoauth2 auth token
|
||||||
*/
|
*/
|
||||||
const encode_xoauth2 = () =>
|
const encode_xoauth2 = (): string =>
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`
|
`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`
|
||||||
).toString('base64');
|
).toString('base64');
|
||||||
|
@ -722,10 +725,8 @@ class SMTP extends EventEmitter {
|
||||||
const preferred = this.authentication;
|
const preferred = this.authentication;
|
||||||
let auth = '';
|
let auth = '';
|
||||||
|
|
||||||
if (this.features && this.features.auth) {
|
if (typeof this.features?.auth === 'string') {
|
||||||
if (typeof this.features.auth === 'string') {
|
auth = this.features.auth;
|
||||||
auth = this.features.auth;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < preferred.length; i++) {
|
for (let i = 0; i < preferred.length; i++) {
|
||||||
|
@ -742,12 +743,17 @@ class SMTP extends EventEmitter {
|
||||||
* @param {*} data data
|
* @param {*} data data
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const failed = (err, data) => {
|
const failed = (err: Error, data: unknown) => {
|
||||||
this.loggedin = false;
|
this.loggedin = false;
|
||||||
this.close(); // if auth is bad, close the connection, it won't get better by itself
|
this.close(); // if auth is bad, close the connection, it won't get better by itself
|
||||||
caller(
|
caller(
|
||||||
callback,
|
callback,
|
||||||
SMTPError('authorization.failed', SMTPError.AUTHFAILED, err, data)
|
makeSMTPError(
|
||||||
|
'authorization.failed',
|
||||||
|
SMTPErrorStates.AUTHFAILED,
|
||||||
|
err,
|
||||||
|
data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -756,7 +762,7 @@ class SMTP extends EventEmitter {
|
||||||
* @param {*} data data
|
* @param {*} data data
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const response = (err, data) => {
|
const response = (err: Error | null | undefined, data: unknown) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
failed(err, data);
|
failed(err, data);
|
||||||
} else {
|
} else {
|
||||||
|
@ -771,11 +777,15 @@ class SMTP extends EventEmitter {
|
||||||
* @param {string} msg msg
|
* @param {string} msg msg
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const attempt = (err, data, msg) => {
|
const attempt = (
|
||||||
|
err: Error | null | undefined,
|
||||||
|
data: unknown,
|
||||||
|
msg: string
|
||||||
|
) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
failed(err, data);
|
failed(err, data);
|
||||||
} else {
|
} else {
|
||||||
if (method === AUTH_METHODS.CRAM_MD5) {
|
if (method === AUTH_METHODS['CRAM-MD5']) {
|
||||||
this.command(encode_cram_md5(msg), response, [235, 503]);
|
this.command(encode_cram_md5(msg), response, [235, 503]);
|
||||||
} else if (method === AUTH_METHODS.LOGIN) {
|
} else if (method === AUTH_METHODS.LOGIN) {
|
||||||
this.command(
|
this.command(
|
||||||
|
@ -793,7 +803,7 @@ class SMTP extends EventEmitter {
|
||||||
* @param {string} msg msg
|
* @param {string} msg msg
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const attempt_user = (err, data, msg) => {
|
const attempt_user = (err: Error, data: unknown) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
failed(err, data);
|
failed(err, data);
|
||||||
} else {
|
} else {
|
||||||
|
@ -808,8 +818,8 @@ class SMTP extends EventEmitter {
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AUTH_METHODS.CRAM_MD5:
|
case AUTH_METHODS['CRAM-MD5']:
|
||||||
this.command(`AUTH ${AUTH_METHODS.CRAM_MD5}`, attempt, [334]);
|
this.command(`AUTH ${AUTH_METHODS['CRAM-MD5']}`, attempt, [334]);
|
||||||
break;
|
break;
|
||||||
case AUTH_METHODS.LOGIN:
|
case AUTH_METHODS.LOGIN:
|
||||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
|
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
|
||||||
|
@ -830,7 +840,12 @@ class SMTP extends EventEmitter {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
const msg = 'no form of authorization supported';
|
const msg = 'no form of authorization supported';
|
||||||
const err = SMTPError(msg, SMTPError.AUTHNOTSUPPORTED, null, data);
|
const err = makeSMTPError(
|
||||||
|
msg,
|
||||||
|
SMTPErrorStates.AUTHNOTSUPPORTED,
|
||||||
|
null,
|
||||||
|
data
|
||||||
|
);
|
||||||
caller(callback, err);
|
caller(callback, err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -840,10 +855,11 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {boolean} [force=false] whether or not to force destroy the connection
|
* @param {boolean} [force=false] whether or not to force destroy the connection
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
close(force = false) {
|
public close(force = false) {
|
||||||
if (this.sock) {
|
if (this.sock) {
|
||||||
if (force) {
|
if (force) {
|
||||||
this.log('smtp connection destroyed!');
|
this.log('smtp connection destroyed!');
|
||||||
|
@ -867,10 +883,11 @@ class SMTP extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @public
|
||||||
* @param {function(...*): void} [callback] function to call after response
|
* @param {function(...*): void} [callback] function to call after response
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
quit(callback) {
|
public quit(callback?: (...rest: any[]) => void) {
|
||||||
this.command(
|
this.command(
|
||||||
'quit',
|
'quit',
|
||||||
(err, data) => {
|
(err, data) => {
|
||||||
|
@ -881,8 +898,3 @@ class SMTP extends EventEmitter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.SMTP = SMTP;
|
|
||||||
exports.state = SMTPState;
|
|
||||||
exports.authentication = AUTH_METHODS;
|
|
||||||
exports.DEFAULT_TIMEOUT = TIMEOUT;
|
|
12
smtp/typings.d.ts
vendored
Normal file
12
smtp/typings.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/* eslint-disable no-var */
|
||||||
|
declare module 'addressparser' {
|
||||||
|
var addressparser: (address?: string) => { name: string; address: string }[];
|
||||||
|
export = addressparser;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'emailjs-mime-codec' {
|
||||||
|
var codec: {
|
||||||
|
mimeWordEncode: (word?: string) => string;
|
||||||
|
};
|
||||||
|
export = codec;
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
describe('authorize plain', function() {
|
|
||||||
const { simpleParser: parser } = require('mailparser');
|
|
||||||
const { SMTPServer: smtpServer } = require('smtp-server');
|
|
||||||
const { expect } = require('chai');
|
|
||||||
const email = require('../email');
|
|
||||||
const port = 2526;
|
|
||||||
|
|
||||||
let server = null;
|
|
||||||
let smtp = null;
|
|
||||||
|
|
||||||
const send = function(message, verify, done) {
|
|
||||||
smtp.onData = function(stream, session, callback) {
|
|
||||||
parser(stream)
|
|
||||||
.then(verify)
|
|
||||||
.then(done)
|
|
||||||
.catch(done);
|
|
||||||
stream.on('end', callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
server.send(message, function(err) {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
before(function(done) {
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
|
|
||||||
|
|
||||||
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
|
|
||||||
smtp.listen(port, function() {
|
|
||||||
smtp.onAuth = function(auth, session, callback) {
|
|
||||||
if (auth.username == 'pooh' && auth.password == 'honey') {
|
|
||||||
callback(null, { user: 'pooh' });
|
|
||||||
} else {
|
|
||||||
return callback(new Error('invalid user / pass'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
server = email.server.connect({
|
|
||||||
port: port,
|
|
||||||
user: 'pooh',
|
|
||||||
password: 'honey',
|
|
||||||
ssl: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function(done) {
|
|
||||||
smtp.close(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('login', function(done) {
|
|
||||||
const message = {
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'piglet@gmail.com',
|
|
||||||
to: 'pooh@gmail.com',
|
|
||||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const created = email.message.create(message);
|
|
||||||
|
|
||||||
const callback = function(mail) {
|
|
||||||
expect(mail.text).to.equal(message.text + '\n\n\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
};
|
|
||||||
|
|
||||||
send(created, callback, done);
|
|
||||||
});
|
|
||||||
});
|
|
70
test/authplain.ts
Normal file
70
test/authplain.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import test from 'ava';
|
||||||
|
import mailparser from 'mailparser';
|
||||||
|
import smtp from 'smtp-server';
|
||||||
|
|
||||||
|
import { client as c, message as m } from '../email';
|
||||||
|
|
||||||
|
type UnPromisify<T> = T extends Promise<infer U> ? U : T;
|
||||||
|
|
||||||
|
const port = 2526;
|
||||||
|
const client = new c.Client({
|
||||||
|
port,
|
||||||
|
user: 'pooh',
|
||||||
|
password: 'honey',
|
||||||
|
ssl: true,
|
||||||
|
});
|
||||||
|
const server = new smtp.SMTPServer({ secure: true, authMethods: ['LOGIN'] });
|
||||||
|
|
||||||
|
const send = (
|
||||||
|
message: m.Message,
|
||||||
|
verify: (
|
||||||
|
mail: UnPromisify<ReturnType<typeof mailparser.simpleParser>>
|
||||||
|
) => void,
|
||||||
|
done: () => void
|
||||||
|
) => {
|
||||||
|
server.onData = (stream: Readable, _session, callback: () => void) => {
|
||||||
|
mailparser.simpleParser(stream).then(verify).then(done).catch(done);
|
||||||
|
stream.on('end', callback);
|
||||||
|
};
|
||||||
|
client.send(message, (err) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test.before.cb((t) => {
|
||||||
|
server.listen(port, function () {
|
||||||
|
server.onAuth = function (auth, _session, callback) {
|
||||||
|
if (auth.username == 'pooh' && auth.password == 'honey') {
|
||||||
|
callback(null, { user: 'pooh' });
|
||||||
|
} else {
|
||||||
|
return callback(new Error('invalid user / pass'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after.cb((t) => server.close(t.end));
|
||||||
|
|
||||||
|
test.cb('authorize plain', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this is a test TEXT message from emailjs',
|
||||||
|
from: 'piglet@gmail.com',
|
||||||
|
to: 'pooh@gmail.com',
|
||||||
|
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.end
|
||||||
|
);
|
||||||
|
});
|
|
@ -1,73 +0,0 @@
|
||||||
describe('authorize ssl', function() {
|
|
||||||
const { simpleParser: parser } = require('mailparser');
|
|
||||||
const { SMTPServer: smtpServer } = require('smtp-server');
|
|
||||||
const { expect } = require('chai');
|
|
||||||
const email = require('../email');
|
|
||||||
const port = 2526;
|
|
||||||
|
|
||||||
let server = null;
|
|
||||||
let smtp = null;
|
|
||||||
|
|
||||||
const send = function(message, verify, done) {
|
|
||||||
smtp.onData = function(stream, session, callback) {
|
|
||||||
parser(stream)
|
|
||||||
.then(verify)
|
|
||||||
.then(done)
|
|
||||||
.catch(done);
|
|
||||||
stream.on('end', callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
server.send(message, function(err) {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
before(function(done) {
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
|
|
||||||
|
|
||||||
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
|
|
||||||
smtp.listen(port, function() {
|
|
||||||
smtp.onAuth = function(auth, session, callback) {
|
|
||||||
if (auth.username == 'pooh' && auth.password == 'honey') {
|
|
||||||
callback(null, { user: 'pooh' });
|
|
||||||
} else {
|
|
||||||
return callback(new Error('invalid user / pass'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
server = email.server.connect({
|
|
||||||
port: port,
|
|
||||||
user: 'pooh',
|
|
||||||
password: 'honey',
|
|
||||||
ssl: true,
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function(done) {
|
|
||||||
smtp.close(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('login', function(done) {
|
|
||||||
const message = {
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'pooh@gmail.com',
|
|
||||||
to: 'rabbit@gmail.com',
|
|
||||||
text: 'hello friend, i hope this message finds you well.',
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
email.message.create(message),
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal(message.text + '\n\n\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
70
test/authssl.ts
Normal file
70
test/authssl.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import test from 'ava';
|
||||||
|
import mailparser from 'mailparser';
|
||||||
|
import smtp from 'smtp-server';
|
||||||
|
|
||||||
|
import { client as c, message as m } from '../email';
|
||||||
|
|
||||||
|
const port = 2526;
|
||||||
|
|
||||||
|
const client = new c.Client({
|
||||||
|
port,
|
||||||
|
user: 'pooh',
|
||||||
|
password: 'honey',
|
||||||
|
ssl: true,
|
||||||
|
});
|
||||||
|
const server = new smtp.SMTPServer({ secure: true, authMethods: ['LOGIN'] });
|
||||||
|
|
||||||
|
type UnPromisify<T> = T extends Promise<infer U> ? U : T;
|
||||||
|
const send = (
|
||||||
|
message: m.Message,
|
||||||
|
verify: (
|
||||||
|
mail: UnPromisify<ReturnType<typeof mailparser.simpleParser>>
|
||||||
|
) => void,
|
||||||
|
done: () => void
|
||||||
|
) => {
|
||||||
|
server.onData = (stream: Readable, _session, callback: () => void) => {
|
||||||
|
mailparser.simpleParser(stream).then(verify).then(done).catch(done);
|
||||||
|
stream.on('end', callback);
|
||||||
|
};
|
||||||
|
client.send(message, (err) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test.before.cb((t) => {
|
||||||
|
server.listen(port, function () {
|
||||||
|
server.onAuth = function (auth, _session, callback) {
|
||||||
|
if (auth.username == 'pooh' && auth.password == 'honey') {
|
||||||
|
callback(null, { user: 'pooh' });
|
||||||
|
} else {
|
||||||
|
return callback(new Error('invalid user / pass'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after.cb((t) => server.close(t.end));
|
||||||
|
|
||||||
|
test.cb('authorize ssl', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this is a test TEXT message from emailjs',
|
||||||
|
from: 'pooh@gmail.com',
|
||||||
|
to: 'rabbit@gmail.com',
|
||||||
|
text: 'hello friend, i hope this message finds you well.',
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.end
|
||||||
|
);
|
||||||
|
});
|
41
test/date.js
41
test/date.js
|
@ -1,41 +0,0 @@
|
||||||
describe('rfc2822 dates', function() {
|
|
||||||
const { expect } = require('chai');
|
|
||||||
const {
|
|
||||||
date: { getRFC2822Date, getRFC2822DateUTC },
|
|
||||||
} = require('../email');
|
|
||||||
|
|
||||||
var d_utc = dt => getRFC2822DateUTC(new Date(dt));
|
|
||||||
var d = (dt, utc = false) => getRFC2822Date(new Date(dt), utc);
|
|
||||||
|
|
||||||
it('should match standard regex', function(done) {
|
|
||||||
// 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
|
|
||||||
var 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}))$/;
|
|
||||||
expect(d(0)).to.match(rfc2822re);
|
|
||||||
expect(d(329629726785)).to.match(rfc2822re);
|
|
||||||
expect(d(729629726785)).to.match(rfc2822re);
|
|
||||||
expect(d(1129629726785)).to.match(rfc2822re);
|
|
||||||
expect(d(1529629726785)).to.match(rfc2822re);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should produce proper UTC dates', function(done) {
|
|
||||||
expect(d_utc(0)).to.equal('Thu, 01 Jan 1970 00:00:00 +0000');
|
|
||||||
expect(d_utc(0)).to.equal(d(0, true));
|
|
||||||
|
|
||||||
expect(d_utc(329629726785)).to.equal('Thu, 12 Jun 1980 03:48:46 +0000');
|
|
||||||
expect(d_utc(329629726785)).to.equal(d(329629726785, true));
|
|
||||||
|
|
||||||
expect(d_utc(729629726785)).to.equal('Sat, 13 Feb 1993 18:55:26 +0000');
|
|
||||||
expect(d_utc(729629726785)).to.equal(d(729629726785, true));
|
|
||||||
|
|
||||||
expect(d_utc(1129629726785)).to.equal('Tue, 18 Oct 2005 10:02:06 +0000');
|
|
||||||
expect(d_utc(1129629726785)).to.equal(d(1129629726785, true));
|
|
||||||
|
|
||||||
expect(d_utc(1529629726785)).to.equal('Fri, 22 Jun 2018 01:08:46 +0000');
|
|
||||||
expect(d_utc(1529629726785)).to.equal(d(1529629726785, true));
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
35
test/date.ts
Normal file
35
test/date.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import test from 'ava';
|
||||||
|
|
||||||
|
import { date as d } from '../email';
|
||||||
|
|
||||||
|
const { getRFC2822Date, getRFC2822DateUTC } = d;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rfc2822 UTC', async (t) => {
|
||||||
|
t.is(toD_utc(0), 'Thu, 01 Jan 1970 00:00:00 +0000');
|
||||||
|
t.is(toD_utc(0), toD(0, true));
|
||||||
|
|
||||||
|
t.is(toD_utc(329629726785), 'Thu, 12 Jun 1980 03:48:46 +0000');
|
||||||
|
t.is(toD_utc(329629726785), toD(329629726785, true));
|
||||||
|
|
||||||
|
t.is(toD_utc(729629726785), 'Sat, 13 Feb 1993 18:55:26 +0000');
|
||||||
|
t.is(toD_utc(729629726785), toD(729629726785, true));
|
||||||
|
|
||||||
|
t.is(toD_utc(1129629726785), 'Tue, 18 Oct 2005 10:02:06 +0000');
|
||||||
|
t.is(toD_utc(1129629726785), toD(1129629726785, true));
|
||||||
|
|
||||||
|
t.is(toD_utc(1529629726785), 'Fri, 22 Jun 2018 01:08:46 +0000');
|
||||||
|
t.is(toD_utc(1529629726785), toD(1529629726785, true));
|
||||||
|
});
|
461
test/message.js
461
test/message.js
|
@ -1,461 +0,0 @@
|
||||||
describe('messages', function() {
|
|
||||||
const { simpleParser: parser } = require('mailparser');
|
|
||||||
const { SMTPServer: smtpServer } = require('smtp-server');
|
|
||||||
const { expect } = require('chai');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const email = require('../email');
|
|
||||||
const port = 2526;
|
|
||||||
|
|
||||||
let server = null;
|
|
||||||
let smtp = null;
|
|
||||||
|
|
||||||
const send = function(message, verify, done) {
|
|
||||||
smtp.onData = function(stream, session, callback) {
|
|
||||||
//stream.pipe(process.stdout);
|
|
||||||
parser(stream)
|
|
||||||
.then(verify)
|
|
||||||
.then(done)
|
|
||||||
.catch(done);
|
|
||||||
stream.on('end', callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
server.send(message, function(err) {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
before(function(done) {
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // prevent CERT_HAS_EXPIRED errors
|
|
||||||
|
|
||||||
smtp = new smtpServer({ secure: true, authMethods: ['LOGIN'] });
|
|
||||||
smtp.listen(port, function() {
|
|
||||||
smtp.onAuth = function(auth, session, callback) {
|
|
||||||
if (auth.username == 'pooh' && auth.password == 'honey') {
|
|
||||||
callback(null, { user: 'pooh' });
|
|
||||||
} else {
|
|
||||||
return callback(new Error('invalid user / pass'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
server = email.server.connect({
|
|
||||||
port: port,
|
|
||||||
user: 'pooh',
|
|
||||||
password: 'honey',
|
|
||||||
ssl: true,
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function(done) {
|
|
||||||
smtp.close(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('simple text message', function(done) {
|
|
||||||
var message = {
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'zelda@gmail.com',
|
|
||||||
to: 'gannon@gmail.com',
|
|
||||||
text: 'hello friend, i hope this message finds you well.',
|
|
||||||
'message-id': 'this is a special id',
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
email.message.create(message),
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal(message.text + '\n\n\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
expect(mail.messageId).to.equal('<' + message['message-id'] + '>');
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('null text', function(done) {
|
|
||||||
send(
|
|
||||||
{
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'zelda@gmail.com',
|
|
||||||
to: 'gannon@gmail.com',
|
|
||||||
text: null,
|
|
||||||
'message-id': 'this is a special id',
|
|
||||||
},
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal('\n\n\n');
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('empty text', function(done) {
|
|
||||||
send(
|
|
||||||
{
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'zelda@gmail.com',
|
|
||||||
to: 'gannon@gmail.com',
|
|
||||||
text: '',
|
|
||||||
'message-id': 'this is a special id',
|
|
||||||
},
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal('\n\n\n');
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('simple unicode text message', function(done) {
|
|
||||||
var message = {
|
|
||||||
subject: 'this ✓ is a test ✓ TEXT message from emailjs',
|
|
||||||
from: 'zelda✓ <zelda@gmail.com>',
|
|
||||||
to: 'gannon✓ <gannon@gmail.com>',
|
|
||||||
text: 'hello ✓ friend, i hope this message finds you well.',
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
email.message.create(message),
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal(message.text + '\n\n\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('very large text message', function(done) {
|
|
||||||
this.timeout(20000);
|
|
||||||
// thanks to jart+loberstech for this one!
|
|
||||||
var message = {
|
|
||||||
subject: 'this is a test TEXT message from emailjs',
|
|
||||||
from: 'ninjas@gmail.com',
|
|
||||||
to: 'pirates@gmail.com',
|
|
||||||
text: fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/smtp.txt'),
|
|
||||||
'utf-8'
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
email.message.create(message),
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.text).to.equal(message.text.replace(/\r/g, '') + '\n\n\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('very large text data', function(done) {
|
|
||||||
this.timeout(10000);
|
|
||||||
var text =
|
|
||||||
'<html><body><pre>' +
|
|
||||||
fs.readFileSync(path.join(__dirname, 'attachments/smtp.txt'), 'utf-8') +
|
|
||||||
'</pre></body></html>';
|
|
||||||
var message = {
|
|
||||||
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.',
|
|
||||||
attachment: { data: text, alternative: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
message,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.html).to.equal(text.replace(/\r/g, ''));
|
|
||||||
expect(mail.text).to.equal(message.text + '\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('html data', function(done) {
|
|
||||||
var html = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/smtp.html'),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
var message = {
|
|
||||||
subject: 'this is a test TEXT+HTML+DATA message from emailjs',
|
|
||||||
from: 'obama@gmail.com',
|
|
||||||
to: 'mitt@gmail.com',
|
|
||||||
attachment: { data: html, alternative: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
message,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.html).to.equal(html.replace(/\r/g, ''));
|
|
||||||
expect(mail.text).to.equal('\n');
|
|
||||||
expect(mail.subject).to.equal(message.subject);
|
|
||||||
expect(mail.from.text).to.equal(message.from);
|
|
||||||
expect(mail.to.text).to.equal(message.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('html file', function(done) {
|
|
||||||
var html = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/smtp.html'),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
var headers = {
|
|
||||||
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
|
|
||||||
from: 'thomas@gmail.com',
|
|
||||||
to: 'nikolas@gmail.com',
|
|
||||||
attachment: {
|
|
||||||
path: path.join(__dirname, 'attachments/smtp.html'),
|
|
||||||
alternative: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.html).to.equal(html.replace(/\r/g, ''));
|
|
||||||
expect(mail.text).to.equal('\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('html with image embed', function(done) {
|
|
||||||
var html = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/smtp2.html'),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
var image = fs.readFileSync(path.join(__dirname, 'attachments/smtp.gif'));
|
|
||||||
var headers = {
|
|
||||||
subject: 'this is a test TEXT+HTML+IMAGE message from emailjs',
|
|
||||||
from: 'ninja@gmail.com',
|
|
||||||
to: 'pirate@gmail.com',
|
|
||||||
attachment: {
|
|
||||||
path: path.join(__dirname, 'attachments/smtp2.html'),
|
|
||||||
alternative: true,
|
|
||||||
related: [
|
|
||||||
{
|
|
||||||
path: path.join(__dirname, 'attachments/smtp.gif'),
|
|
||||||
type: 'image/gif',
|
|
||||||
name: 'smtp-diagram.gif',
|
|
||||||
headers: { 'Content-ID': '<smtp-diagram@local>' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.attachments[0].content.toString('base64')).to.equal(
|
|
||||||
image.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.html).to.equal(html.replace(/\r/g, ''));
|
|
||||||
expect(mail.text).to.equal('\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('html data and attachment', function(done) {
|
|
||||||
var html = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/smtp.html'),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
var headers = {
|
|
||||||
subject: 'this is a test TEXT+HTML+FILE message from emailjs',
|
|
||||||
from: 'thomas@gmail.com',
|
|
||||||
to: 'nikolas@gmail.com',
|
|
||||||
attachment: [
|
|
||||||
{
|
|
||||||
path: path.join(__dirname, 'attachments/smtp.html'),
|
|
||||||
alternative: true,
|
|
||||||
},
|
|
||||||
{ path: path.join(__dirname, 'attachments/smtp.gif') },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.html).to.equal(html.replace(/\r/g, ''));
|
|
||||||
expect(mail.text).to.equal('\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('attachment', function(done) {
|
|
||||||
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
|
|
||||||
var headers = {
|
|
||||||
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: path.join(__dirname, 'attachments/smtp.pdf'),
|
|
||||||
type: 'application/pdf',
|
|
||||||
name: 'smtp-info.pdf',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.attachments[0].content.toString('base64')).to.equal(
|
|
||||||
pdf.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.text).to.equal(headers.text + '\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('attachment sent with unicode filename', function(done) {
|
|
||||||
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
|
|
||||||
var headers = {
|
|
||||||
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: path.join(__dirname, 'attachments/smtp.pdf'),
|
|
||||||
type: 'application/pdf',
|
|
||||||
name: 'smtp-✓-info.pdf',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.attachments[0].content.toString('base64')).to.equal(
|
|
||||||
pdf.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.attachments[0].filename).to.equal('smtp-✓-info.pdf');
|
|
||||||
expect(mail.text).to.equal(headers.text + '\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('attachments', function(done) {
|
|
||||||
this.timeout(4000); /** simpleParser takes a while on macOS */
|
|
||||||
|
|
||||||
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
|
|
||||||
var tar = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
|
|
||||||
);
|
|
||||||
var headers = {
|
|
||||||
subject: 'this is a test TEXT+2+ATTACHMENTS message from emailjs',
|
|
||||||
from: 'sergey@gmail.com',
|
|
||||||
to: 'jobs@gmail.com',
|
|
||||||
text: 'hello friend, i hope this message and attachments finds you well.',
|
|
||||||
attachment: [
|
|
||||||
{
|
|
||||||
path: path.join(__dirname, 'attachments/smtp.pdf'),
|
|
||||||
type: 'application/pdf',
|
|
||||||
name: 'smtp-info.pdf',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz'),
|
|
||||||
type: 'application/tar-gz',
|
|
||||||
name: 'postfix.source.2.8.7.tar.gz',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.attachments[0].content.toString('base64')).to.equal(
|
|
||||||
pdf.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.attachments[1].content.toString('base64')).to.equal(
|
|
||||||
tar.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.text).to.equal(headers.text + '\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('streams', function(done) {
|
|
||||||
this.timeout(4000); /** simpleParser takes a while on macOS */
|
|
||||||
|
|
||||||
var pdf = fs.readFileSync(path.join(__dirname, 'attachments/smtp.pdf'));
|
|
||||||
var tar = fs.readFileSync(
|
|
||||||
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
|
|
||||||
);
|
|
||||||
var stream = fs.createReadStream(
|
|
||||||
path.join(__dirname, 'attachments/smtp.pdf')
|
|
||||||
);
|
|
||||||
var stream2 = fs.createReadStream(
|
|
||||||
path.join(__dirname, 'attachments/postfix-2.8.7.tar.gz')
|
|
||||||
);
|
|
||||||
var headers = {
|
|
||||||
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.',
|
|
||||||
attachment: [
|
|
||||||
{ stream: stream, type: 'application/pdf', name: 'smtp-info.pdf' },
|
|
||||||
{
|
|
||||||
stream: stream2,
|
|
||||||
type: 'application/x-gzip',
|
|
||||||
name: 'postfix.source.2.8.7.tar.gz',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
stream.pause();
|
|
||||||
stream2.pause();
|
|
||||||
|
|
||||||
send(
|
|
||||||
headers,
|
|
||||||
function(mail) {
|
|
||||||
expect(mail.attachments[0].content.toString('base64')).to.equal(
|
|
||||||
pdf.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.attachments[1].content.toString('base64')).to.equal(
|
|
||||||
tar.toString('base64')
|
|
||||||
);
|
|
||||||
expect(mail.text).to.equal(headers.text + '\n');
|
|
||||||
expect(mail.subject).to.equal(headers.subject);
|
|
||||||
expect(mail.from.text).to.equal(headers.from);
|
|
||||||
expect(mail.to.text).to.equal(headers.to);
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
449
test/message.ts
Normal file
449
test/message.ts
Normal file
|
@ -0,0 +1,449 @@
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import { readFileSync, createReadStream } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import test from 'ava';
|
||||||
|
import mailparser from 'mailparser';
|
||||||
|
import smtp from 'smtp-server';
|
||||||
|
|
||||||
|
import { client as c, message as m } from '../email';
|
||||||
|
|
||||||
|
const port = 2526;
|
||||||
|
const client = new c.Client({
|
||||||
|
port,
|
||||||
|
user: 'pooh',
|
||||||
|
password: 'honey',
|
||||||
|
ssl: true,
|
||||||
|
});
|
||||||
|
const server = new smtp.SMTPServer({ secure: true, authMethods: ['LOGIN'] });
|
||||||
|
|
||||||
|
type UnPromisify<T> = T extends Promise<infer U> ? U : T;
|
||||||
|
const send = (
|
||||||
|
message: m.Message,
|
||||||
|
verify: (
|
||||||
|
mail: UnPromisify<ReturnType<typeof mailparser.simpleParser>>
|
||||||
|
) => void,
|
||||||
|
done: () => void
|
||||||
|
) => {
|
||||||
|
server.onData = (stream: Readable, _session, callback: () => void) => {
|
||||||
|
mailparser
|
||||||
|
.simpleParser(stream, { skipTextLinks: true } as Record<string, unknown>)
|
||||||
|
.then(verify)
|
||||||
|
.then(done)
|
||||||
|
.catch(done);
|
||||||
|
stream.on('end', callback);
|
||||||
|
};
|
||||||
|
client.send(message, (err) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test.before.cb((t) => {
|
||||||
|
server.listen(port, function () {
|
||||||
|
server.onAuth = function (auth, _session, callback) {
|
||||||
|
if (auth.username == 'pooh' && auth.password == 'honey') {
|
||||||
|
callback(null, { user: 'pooh' });
|
||||||
|
} else {
|
||||||
|
return callback(new Error('invalid user / pass'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after.cb((t) => server.close(t.end));
|
||||||
|
|
||||||
|
test.cb('simple text message', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this is a test TEXT message from emailjs',
|
||||||
|
from: 'zelda@gmail.com',
|
||||||
|
to: 'gannon@gmail.com',
|
||||||
|
text: 'hello friend, i hope this message finds you well.',
|
||||||
|
'message-id': 'this is a special id',
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.messageId, '<' + msg['message-id'] + '>');
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('null text message', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this is a test TEXT message from emailjs',
|
||||||
|
from: 'zelda@gmail.com',
|
||||||
|
to: 'gannon@gmail.com',
|
||||||
|
text: null,
|
||||||
|
'message-id': 'this is a special id',
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.text, '\n\n\n');
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('empty text message', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this is a test TEXT message from emailjs',
|
||||||
|
from: 'zelda@gmail.com',
|
||||||
|
to: 'gannon@gmail.com',
|
||||||
|
text: '',
|
||||||
|
'message-id': 'this is a special id',
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.text, '\n\n\n');
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('simple unicode text message', (t) => {
|
||||||
|
const msg = {
|
||||||
|
subject: 'this ✓ is a test ✓ TEXT message from emailjs',
|
||||||
|
from: 'zelda✓ <zelda@gmail.com>',
|
||||||
|
to: 'gannon✓ <gannon@gmail.com>',
|
||||||
|
text: 'hello ✓ friend, i hope this message finds you well.',
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('very large text message', (t) => {
|
||||||
|
// thanks to jart+loberstech for this one!
|
||||||
|
const msg = {
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.text, msg.text.replace(/\r/g, '') + '\n\n\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('very large text data message', (t) => {
|
||||||
|
const text =
|
||||||
|
'<html><body><pre>' +
|
||||||
|
readFileSync(join(__dirname, 'attachments/smtp.txt'), 'utf-8') +
|
||||||
|
'</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.',
|
||||||
|
attachment: ({
|
||||||
|
data: text,
|
||||||
|
alternative: true,
|
||||||
|
} as unknown) as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.html, text.replace(/\r/g, ''));
|
||||||
|
t.is(mail.text, msg.text + '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('html data message', (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,
|
||||||
|
alternative: true,
|
||||||
|
} as unknown) as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.html, html.replace(/\r/g, ''));
|
||||||
|
t.is(mail.text, '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('html file message', (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,
|
||||||
|
} as unknown) as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.html, html.replace(/\r/g, ''));
|
||||||
|
t.is(mail.text, '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('html with image embed message', (t) => {
|
||||||
|
const html = readFileSync(join(__dirname, 'attachments/smtp2.html'), 'utf-8');
|
||||||
|
const image = readFileSync(join(__dirname, 'attachments/smtp.gif'));
|
||||||
|
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'),
|
||||||
|
alternative: true,
|
||||||
|
related: [
|
||||||
|
{
|
||||||
|
path: join(__dirname, 'attachments/smtp.gif'),
|
||||||
|
type: 'image/gif',
|
||||||
|
name: 'smtp-diagram.gif',
|
||||||
|
headers: { 'Content-ID': '<smtp-diagram@local>' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown) as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(
|
||||||
|
mail.attachments[0].content.toString('base64'),
|
||||||
|
image.toString('base64')
|
||||||
|
);
|
||||||
|
t.is(mail.html, html.replace(/\r/g, ''));
|
||||||
|
t.is(mail.text, '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('html data and attachment message', (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') },
|
||||||
|
] as m.MessageAttachment[],
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(mail.html, html.replace(/\r/g, ''));
|
||||||
|
t.is(mail.text, '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('attachment message', (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'),
|
||||||
|
type: 'application/pdf',
|
||||||
|
name: 'smtp-info.pdf',
|
||||||
|
} as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(
|
||||||
|
mail.attachments[0].content.toString('base64'),
|
||||||
|
pdf.toString('base64')
|
||||||
|
);
|
||||||
|
t.is(mail.text, msg.text + '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('attachment sent with unicode filename message', (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'),
|
||||||
|
type: 'application/pdf',
|
||||||
|
name: 'smtp-✓-info.pdf',
|
||||||
|
} as m.MessageAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
t.is(
|
||||||
|
mail.attachments[0].content.toString('base64'),
|
||||||
|
pdf.toString('base64')
|
||||||
|
);
|
||||||
|
t.is(mail.attachments[0].filename, 'smtp-✓-info.pdf');
|
||||||
|
t.is(mail.text, msg.text + '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('attachments message', (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',
|
||||||
|
to: 'jobs@gmail.com',
|
||||||
|
text: 'hello friend, i hope this message and attachments finds you well.',
|
||||||
|
attachment: [
|
||||||
|
{
|
||||||
|
path: join(__dirname, 'attachments/smtp.pdf'),
|
||||||
|
type: 'application/pdf',
|
||||||
|
name: 'smtp-info.pdf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: join(__dirname, 'attachments/postfix-2.8.7.tar.gz'),
|
||||||
|
type: 'application/tar-gz',
|
||||||
|
name: 'postfix.source.2.8.7.tar.gz',
|
||||||
|
},
|
||||||
|
] as m.MessageAttachment[],
|
||||||
|
};
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.text, msg.text + '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('streams message', (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.',
|
||||||
|
attachment: ([
|
||||||
|
{ stream, type: 'application/pdf', name: 'smtp-info.pdf' },
|
||||||
|
{
|
||||||
|
stream: stream2,
|
||||||
|
type: 'application/x-gzip',
|
||||||
|
name: 'postfix.source.2.8.7.tar.gz',
|
||||||
|
},
|
||||||
|
] as unknown) as m.MessageAttachment[],
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.pause();
|
||||||
|
stream2.pause();
|
||||||
|
|
||||||
|
send(
|
||||||
|
new m.Message(msg),
|
||||||
|
(mail) => {
|
||||||
|
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.text, msg.text + '\n');
|
||||||
|
t.is(mail.subject, msg.subject);
|
||||||
|
t.is(mail.from?.text, msg.from);
|
||||||
|
t.is(mail.to?.text, msg.to);
|
||||||
|
},
|
||||||
|
t.end
|
||||||
|
);
|
||||||
|
});
|
|
@ -1,63 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
describe('Connect to wrong email server', function() {
|
|
||||||
const emailModulePath = require.resolve('../email.js');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {typeof import('../email.js')}
|
|
||||||
*/
|
|
||||||
let email = null;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
if (require.cache[emailModulePath]) {
|
|
||||||
delete require.cache[emailModulePath];
|
|
||||||
}
|
|
||||||
email = require(emailModulePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should not call callback multiple times with wrong server configuration', function(done) {
|
|
||||||
this.timeout(5000);
|
|
||||||
const server = email.server.connect({ host: 'bar.baz' });
|
|
||||||
server.send(
|
|
||||||
{
|
|
||||||
from: 'foo@bar.baz',
|
|
||||||
to: 'foo@bar.baz',
|
|
||||||
subject: 'hello world',
|
|
||||||
text: 'hello world',
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
assert.notEqual(err, null);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have a default timeout', function(done) {
|
|
||||||
const connectionOptions = {
|
|
||||||
user: 'username',
|
|
||||||
password: 'password',
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 1234,
|
|
||||||
};
|
|
||||||
|
|
||||||
const email = require(emailModulePath);
|
|
||||||
assert.strictEqual(
|
|
||||||
email.server.connect(connectionOptions).smtp.timeout,
|
|
||||||
email.SMTP.DEFAULT_TIMEOUT
|
|
||||||
);
|
|
||||||
|
|
||||||
connectionOptions.timeout = null;
|
|
||||||
assert.strictEqual(
|
|
||||||
email.server.connect(connectionOptions).smtp.timeout,
|
|
||||||
email.SMTP.DEFAULT_TIMEOUT
|
|
||||||
);
|
|
||||||
|
|
||||||
connectionOptions.timeout = undefined;
|
|
||||||
assert.strictEqual(
|
|
||||||
email.server.connect(connectionOptions).smtp.timeout,
|
|
||||||
email.SMTP.DEFAULT_TIMEOUT
|
|
||||||
);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
37
test/server.ts
Normal file
37
test/server.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import test from 'ava';
|
||||||
|
|
||||||
|
import { client as c, message as m, smtp as s } from '../email';
|
||||||
|
|
||||||
|
test.cb(
|
||||||
|
'connecting to wrong email server should not invoke callback multiple times',
|
||||||
|
(t) => {
|
||||||
|
const client = new c.Client({ host: 'bar.baz' });
|
||||||
|
const msg = {
|
||||||
|
from: 'foo@bar.baz',
|
||||||
|
to: 'foo@bar.baz',
|
||||||
|
subject: 'hello world',
|
||||||
|
text: 'hello world',
|
||||||
|
};
|
||||||
|
client.send(new m.Message(msg), (err) => {
|
||||||
|
t.not(err, null);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test('should have a default timeout', async (t) => {
|
||||||
|
const connectionOptions = {
|
||||||
|
user: 'username',
|
||||||
|
password: 'password',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 1234,
|
||||||
|
timeout: undefined as number | null | undefined,
|
||||||
|
};
|
||||||
|
t.is(new c.Client(connectionOptions).smtp.timeout, s.DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
connectionOptions.timeout = null;
|
||||||
|
t.is(new c.Client(connectionOptions).smtp.timeout, s.DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
connectionOptions.timeout = undefined;
|
||||||
|
t.is(new c.Client(connectionOptions).smtp.timeout, s.DEFAULT_TIMEOUT);
|
||||||
|
});
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "@ledge/configs/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"*.ts",
|
||||||
|
"smtp/*.ts",
|
||||||
|
"test/*.ts"
|
||||||
|
],
|
||||||
|
"ts-node": {
|
||||||
|
"transpileOnly": true,
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user