mirror of
https://github.com/eleith/emailjs.git
synced 2024-06-14 20:19: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": {
|
||||
"es6": true,
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"mocha"
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended"
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRestArgs": true
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"error",
|
||||
"all"
|
||||
|
@ -30,11 +46,6 @@
|
|||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"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"
|
||||
"valid-jsdoc": "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]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: install
|
||||
run: yarn
|
||||
- name: install
|
||||
run: yarn
|
||||
|
||||
- name: test
|
||||
run: yarn test
|
||||
- name: test
|
||||
run: yarn test
|
||||
|
||||
- name: test-cjs
|
||||
run: yarn test-cjs
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,7 +1,7 @@
|
|||
/.vscode/
|
||||
/node_modules/
|
||||
/test/config.js
|
||||
/npm-debug.log
|
||||
.vscode
|
||||
node_modules
|
||||
|
||||
*.log
|
||||
*.swp
|
||||
*.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
|
||||
|
||||
|
@ -21,119 +21,129 @@ send emails, html and attachments (files, streams and strings) from node.js to a
|
|||
## EXAMPLE USAGE - text only emails
|
||||
|
||||
```javascript
|
||||
var email = require("./path/to/emailjs/email");
|
||||
var server = email.server.connect({
|
||||
user: "username",
|
||||
password:"password",
|
||||
host: "smtp.your-email.com",
|
||||
ssl: true
|
||||
import { client as c } from 'emailjs';
|
||||
|
||||
const client = new c.Client({
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
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
|
||||
server.send({
|
||||
text: "i hope this works",
|
||||
from: "you <username@your-email.com>",
|
||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
||||
cc: "else <else@your-email.com>",
|
||||
subject: "testing emailjs"
|
||||
}, function(err, message) { console.log(err || message); });
|
||||
client.send({
|
||||
text: 'i hope this works',
|
||||
from: 'you <username@your-email.com>',
|
||||
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||
cc: 'else <else@your-email.com>',
|
||||
subject: 'testing emailjs'
|
||||
}, (err, message) => {
|
||||
console.log(err || message);
|
||||
});
|
||||
```
|
||||
|
||||
## EXAMPLE USAGE - html emails and attachments
|
||||
|
||||
```javascript
|
||||
var email = require("./path/to/emailjs/email");
|
||||
var server = email.server.connect({
|
||||
user: "username",
|
||||
password:"password",
|
||||
host: "smtp.your-email.com",
|
||||
ssl: true
|
||||
import { client as c } from 'emailjs';
|
||||
|
||||
const client = new c.Client({
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
host: 'smtp.your-email.com',
|
||||
ssl: true
|
||||
});
|
||||
|
||||
var message = {
|
||||
text: "i hope this works",
|
||||
from: "you <username@your-email.com>",
|
||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
||||
cc: "else <else@your-email.com>",
|
||||
subject: "testing emailjs",
|
||||
attachment:
|
||||
[
|
||||
{data:"<html>i <i>hope</i> this works!</html>", alternative:true},
|
||||
{path:"path/to/file.zip", type:"application/zip", name:"renamed.zip"}
|
||||
]
|
||||
const message = {
|
||||
text: 'i hope this works',
|
||||
from: 'you <username@your-email.com>',
|
||||
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||
cc: 'else <else@your-email.com>',
|
||||
subject: 'testing emailjs',
|
||||
attachment: [
|
||||
{ data: '<html>i <i>hope</i> this works!</html>', alternative: true },
|
||||
{ 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
|
||||
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
|
||||
|
||||
// or you can create a new server connection with 'email.server.connect'
|
||||
// to asynchronously send individual emails instead of a queue
|
||||
// or instead of using the built-in client you can create an instance of 'smtp.SMTPConnection'
|
||||
```
|
||||
|
||||
## EXAMPLE USAGE - sending through hotmail/outlook
|
||||
## EXAMPLE USAGE - sending through outlook
|
||||
|
||||
```javascript
|
||||
var email = require("./path/to/emailjs/email");
|
||||
var server = email.server.connect({
|
||||
user: "username",
|
||||
password:"password",
|
||||
host: "smtp-mail.outlook.com",
|
||||
tls: {ciphers: "SSLv3"}
|
||||
import { client as c, message as m } from 'emailjs';
|
||||
|
||||
const client = new c.Client({
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
host: 'smtp-mail.outlook.com',
|
||||
tls: {
|
||||
ciphers: 'SSLv3'
|
||||
}
|
||||
});
|
||||
|
||||
var message = {
|
||||
text: "i hope this works",
|
||||
from: "you <username@outlook.com>",
|
||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
||||
cc: "else <else@your-email.com>",
|
||||
subject: "testing emailjs",
|
||||
attachment:
|
||||
[
|
||||
{data:"<html>i <i>hope</i> this works!</html>", alternative:true},
|
||||
{path:"path/to/file.zip", type:"application/zip", name:"renamed.zip"}
|
||||
]
|
||||
};
|
||||
const message = new m.Message({
|
||||
text: 'i hope this works',
|
||||
from: 'you <username@outlook.com>',
|
||||
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||
cc: 'else <else@your-email.com>',
|
||||
subject: 'testing emailjs',
|
||||
attachment: [
|
||||
{ data: '<html>i <i>hope</i> this works!</html>', alternative: true },
|
||||
{ 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
|
||||
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
|
||||
|
||||
```javascript
|
||||
var email = require("./path/to/emailjs/email");
|
||||
var server = email.server.connect({
|
||||
user: "username",
|
||||
password:"password",
|
||||
host: "smtp-mail.outlook.com",
|
||||
tls: {ciphers: "SSLv3"}
|
||||
import { client as c, message as m } from 'emailjs';
|
||||
|
||||
const client = new c.Client({
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
host: 'smtp-mail.outlook.com',
|
||||
tls: {
|
||||
ciphers: 'SSLv3'
|
||||
}
|
||||
});
|
||||
|
||||
var message = {
|
||||
text: "i hope this works",
|
||||
from: "you <username@outlook.com>",
|
||||
to: "someone <someone@your-email.com>, another <another@your-email.com>",
|
||||
cc: "else <else@your-email.com>",
|
||||
subject: "testing emailjs",
|
||||
attachment:
|
||||
[
|
||||
{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/image.jpg", type:"image/jpg", headers:{"Content-ID":"<my-image>"}}
|
||||
]
|
||||
};
|
||||
const message = new m.Message({
|
||||
text: 'i hope this works',
|
||||
from: 'you <username@outlook.com>',
|
||||
to: 'someone <someone@your-email.com>, another <another@your-email.com>',
|
||||
cc: 'else <else@your-email.com>',
|
||||
subject: 'testing emailjs',
|
||||
attachment: [
|
||||
{ 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/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
|
||||
server.send(message, function(err, message) { console.log(err || message); });
|
||||
client.send(message, (err, message) => {
|
||||
console.log(err || message);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# API
|
||||
|
||||
## email.server.connect(options)
|
||||
## new client.Client(options)
|
||||
|
||||
// options is an object with the following keys
|
||||
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)
|
||||
}
|
||||
|
||||
## email.server.send(message, callback)
|
||||
## client.Client#send(message, callback)
|
||||
|
||||
// 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
|
||||
|
@ -158,7 +168,7 @@ server.send(message, function(err, message) { console.log(err || message); });
|
|||
// callback will be executed with (err, message)
|
||||
// either when message is sent or an error has occurred
|
||||
|
||||
## message
|
||||
## new message.Message(headers)
|
||||
|
||||
// headers is an object ('from' and 'to' are required)
|
||||
// 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
|
||||
}
|
||||
|
||||
## email.SMTP.authentication
|
||||
|
||||
associative array of currently supported SMTP authentication mechanisms
|
||||
|
||||
## attachment
|
||||
## message.Message#attach
|
||||
|
||||
// can be called multiple times, each adding a new attachment
|
||||
// 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
|
||||
}
|
||||
|
||||
## 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
|
||||
|
||||
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",
|
||||
"description": "send text/html emails and attachments (files, streams and strings) from node.js to any smtp server",
|
||||
"version": "2.2.0",
|
||||
"author": "eleith",
|
||||
"contributors": [
|
||||
"izuzak",
|
||||
"Hiverness",
|
||||
"mscdex",
|
||||
"jimmybergman",
|
||||
"zackschuster"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/eleith/emailjs.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"addressparser": "^0.3.2",
|
||||
"emailjs-mime-codec": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^5.1.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-mocha": "^5.1.0",
|
||||
"eslint-plugin-prettier": "^2.6.2",
|
||||
"mailparser": "^2.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
"prettier": "^1.13.7",
|
||||
"rollup": "^0.62.0",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"smtp-server": "^3.4.6"
|
||||
},
|
||||
"engine": [
|
||||
"node >= 6"
|
||||
],
|
||||
"main": "email.js",
|
||||
"scripts": {
|
||||
"rollup": "rollup -c rollup.config.js && npm run rollup:test",
|
||||
"rollup:test": "npm run test -- --file rollup/email.bundle.test.js",
|
||||
"test": "mocha"
|
||||
},
|
||||
"license": "MIT",
|
||||
"eslintIgnore": [
|
||||
"rollup.config.js",
|
||||
"rollup/email.bundle.js",
|
||||
"email.esm.js"
|
||||
],
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": true
|
||||
}
|
||||
"name": "emailjs",
|
||||
"description": "send text/html emails and attachments (files, streams and strings) from node.js to any smtp server",
|
||||
"version": "2.2.0",
|
||||
"author": "eleith",
|
||||
"contributors": [
|
||||
"izuzak",
|
||||
"Hiverness",
|
||||
"mscdex",
|
||||
"jimmybergman",
|
||||
"zackschuster"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/eleith/emailjs.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"addressparser": "1.0.1",
|
||||
"emailjs-mime-codec": "2.0.9"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@ledge/configs": "23.0.0",
|
||||
"@rollup/plugin-commonjs": "12.0.0",
|
||||
"@rollup/plugin-node-resolve": "8.0.0",
|
||||
"@rollup/plugin-typescript": "4.1.2",
|
||||
"@types/mailparser": "2.7.3",
|
||||
"@types/smtp-server": "3.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "3.0.1",
|
||||
"@typescript-eslint/parser": "3.0.1",
|
||||
"ava": "3.8.2",
|
||||
"eslint": "7.1.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"mailparser": "2.7.7",
|
||||
"prettier": "2.0.5",
|
||||
"rollup": "2.10.9",
|
||||
"smtp-server": "3.6.0",
|
||||
"ts-node": "8.10.1",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "3.9.3"
|
||||
},
|
||||
"engine": [
|
||||
"node >= 10"
|
||||
],
|
||||
"files": [
|
||||
"email.ts",
|
||||
"smtp",
|
||||
"rollup"
|
||||
],
|
||||
"main": "./rollup/email.cjs",
|
||||
"types": "./email.ts",
|
||||
"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');
|
||||
const { Message, create } = require('./message');
|
||||
const addressparser = require('addressparser');
|
||||
import addressparser from 'addressparser';
|
||||
import { Message } from './message';
|
||||
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
|
||||
* @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
|
||||
* @param {SMTPConnectionOptions} server smtp options
|
||||
*/
|
||||
constructor(server) {
|
||||
this.smtp = new SMTP(server);
|
||||
constructor(server: Partial<SMTPConnectionOptions>) {
|
||||
this.smtp = new SMTPConnection(server);
|
||||
//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
|
||||
* @param {function(Error, MessageStack): void} callback callback
|
||||
* @public
|
||||
* @param {Message} msg the message to send
|
||||
* @param {function(err: Error, msg: Message): void} callback sss
|
||||
* @returns {void}
|
||||
*/
|
||||
send(msg, callback) {
|
||||
/**
|
||||
* @type {Message}
|
||||
*/
|
||||
const message =
|
||||
public send(msg: Message, callback: (err: Error, msg: Message) => void) {
|
||||
const message: Message | null =
|
||||
msg instanceof Message
|
||||
? msg
|
||||
: this._canMakeMessage(msg)
|
||||
? create(msg)
|
||||
: null;
|
||||
? new Message(msg)
|
||||
: null;
|
||||
|
||||
if (message == null) {
|
||||
callback(
|
||||
new Error('message is not a valid Message instance'),
|
||||
/** @type {MessageStack} */ (msg)
|
||||
);
|
||||
callback(new Error('message is not a valid Message instance'), msg);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -91,8 +57,13 @@ class Client {
|
|||
message,
|
||||
to: addressparser(message.header.to),
|
||||
from: addressparser(message.header.from)[0].address,
|
||||
callback: (callback || function() {}).bind(this),
|
||||
};
|
||||
callback: (
|
||||
callback ||
|
||||
function () {
|
||||
/* ø */
|
||||
}
|
||||
).bind(this),
|
||||
} as MessageStack;
|
||||
|
||||
if (message.header.cc) {
|
||||
stack.to = stack.to.concat(addressparser(message.header.cc));
|
||||
|
@ -114,49 +85,51 @@ class Client {
|
|||
this.queue.push(stack);
|
||||
this._poll();
|
||||
} else {
|
||||
callback(new Error(why), /** @type {MessageStack} */ (msg));
|
||||
callback(new Error(why), msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_poll() {
|
||||
clearTimeout(this.timer);
|
||||
protected _poll() {
|
||||
if (this.timer != null) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length) {
|
||||
if (this.smtp.state() == state.NOTCONNECTED) {
|
||||
if (this.smtp.state() == SMTPState.NOTCONNECTED) {
|
||||
this._connect(this.queue[0]);
|
||||
} else if (
|
||||
this.smtp.state() == state.CONNECTED &&
|
||||
this.smtp.state() == SMTPState.CONNECTED &&
|
||||
!this.sending &&
|
||||
this.ready
|
||||
) {
|
||||
this._sendmail(this.queue.shift());
|
||||
this._sendmail(this.queue.shift() as MessageStack);
|
||||
}
|
||||
}
|
||||
// wait around 1 seconds in case something does come in,
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_connect(stack) {
|
||||
protected _connect(stack: MessageStack) {
|
||||
/**
|
||||
* @param {Error} err callback error
|
||||
* @returns {void}
|
||||
*/
|
||||
const connect = err => {
|
||||
const connect = (err: Error) => {
|
||||
if (!err) {
|
||||
const begin = err => {
|
||||
const begin = (err: Error) => {
|
||||
if (!err) {
|
||||
this.ready = true;
|
||||
this._poll();
|
||||
|
@ -188,11 +161,11 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} msg message stack
|
||||
* @returns {boolean} can make message
|
||||
*/
|
||||
_canMakeMessage(msg) {
|
||||
protected _canMakeMessage(msg: MessageHeaders) {
|
||||
return (
|
||||
msg.from &&
|
||||
(msg.to || msg.cc || msg.bcc) &&
|
||||
|
@ -201,13 +174,15 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @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)) {
|
||||
return attachment.some(att => {
|
||||
return attachment.some((att) => {
|
||||
return this._isAttachmentInlinedHtml(att);
|
||||
});
|
||||
} else {
|
||||
|
@ -216,11 +191,11 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {*} attachment attachment
|
||||
* @returns {boolean} is inlined
|
||||
* @protected
|
||||
* @param {MessageAttachment} attachment attachment
|
||||
* @returns {boolean} whether the attachment is inlined html
|
||||
*/
|
||||
_isAttachmentInlinedHtml(attachment) {
|
||||
protected _isAttachmentInlinedHtml(attachment: MessageAttachment) {
|
||||
return (
|
||||
attachment &&
|
||||
(attachment.data || attachment.path) &&
|
||||
|
@ -229,17 +204,17 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @param {function(MessageStack): void} next next
|
||||
* @returns {function(Error): void} callback
|
||||
*/
|
||||
_sendsmtp(stack, next) {
|
||||
protected _sendsmtp(stack: MessageStack, next: (msg: MessageStack) => void) {
|
||||
/**
|
||||
* @param {Error} [err] error
|
||||
* @returns {void}
|
||||
*/
|
||||
return err => {
|
||||
return (err: Error) => {
|
||||
if (!err && next) {
|
||||
next.apply(this, [stack]);
|
||||
} else {
|
||||
|
@ -251,27 +226,27 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendmail(stack) {
|
||||
protected _sendmail(stack: MessageStack) {
|
||||
const from = stack.returnPath || stack.from;
|
||||
this.sending = true;
|
||||
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendrcpt(stack) {
|
||||
protected _sendrcpt(stack: MessageStack) {
|
||||
if (stack.to == null || typeof stack.to === 'string') {
|
||||
throw new TypeError('stack.to must be array');
|
||||
}
|
||||
|
||||
const to = stack.to.shift().address;
|
||||
const to = stack.to.shift()?.address;
|
||||
this.smtp.rcpt(
|
||||
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
|
||||
`<${to}>`
|
||||
|
@ -279,23 +254,23 @@ class Client {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_senddata(stack) {
|
||||
protected _senddata(stack: MessageStack) {
|
||||
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendmessage(stack) {
|
||||
protected _sendmessage(stack: MessageStack) {
|
||||
const stream = stack.message.stream();
|
||||
|
||||
stream.on('data', data => this.smtp.message(data));
|
||||
stream.on('data', (data) => this.smtp.message(data));
|
||||
stream.on('end', () => {
|
||||
this.smtp.data_end(
|
||||
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,
|
||||
// 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._senddone(err, stack);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
* @param {Error} err err
|
||||
* @param {MessageStack} stack stack
|
||||
* @returns {void}
|
||||
*/
|
||||
_senddone(err, stack) {
|
||||
protected _senddone(err: Error | null, stack: MessageStack) {
|
||||
this.sending = false;
|
||||
stack.callback(err, stack.message);
|
||||
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 {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
|
||||
*/
|
||||
function getRFC2822Date(date = new Date(), useUtc = false) {
|
||||
export function getRFC2822Date(date = new Date(), useUtc = false) {
|
||||
if (useUtc) {
|
||||
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)
|
||||
* @returns {string} the converted date
|
||||
*/
|
||||
function getRFC2822DateUTC(date = new Date()) {
|
||||
export function getRFC2822DateUTC(date = new Date()) {
|
||||
const dates = date.toUTCString().split(' ');
|
||||
dates.pop(); // remove timezone
|
||||
dates.push('+0000');
|
||||
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');
|
||||
const { hostname } = require('os');
|
||||
const { Stream } = require('stream');
|
||||
const addressparser = require('addressparser');
|
||||
const { mimeWordEncode } = require('emailjs-mime-codec');
|
||||
const { getRFC2822Date } = require('./date');
|
||||
import fs from 'fs';
|
||||
import type { PathLike } from 'fs';
|
||||
import { hostname } from 'os';
|
||||
import { Stream } from 'stream';
|
||||
import type { Duplex } from 'stream';
|
||||
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.
|
||||
* @type {76}
|
||||
*/
|
||||
const MIMECHUNK = 76;
|
||||
export const MIMECHUNK = 76 as const;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* @returns {string} the generated boundary
|
||||
*/
|
||||
function generate_boundary() {
|
||||
let text = '';
|
||||
const possible =
|
||||
|
@ -45,12 +88,8 @@ function generate_boundary() {
|
|||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} l the person to parse into an address
|
||||
* @returns {string} the parsed address
|
||||
*/
|
||||
function person2address(l) {
|
||||
return addressparser(l)
|
||||
function convertPersonToAddress(person: string) {
|
||||
return addressparser(person)
|
||||
.map(({ name, address }) => {
|
||||
return name
|
||||
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
|
||||
|
@ -59,46 +98,31 @@ function person2address(l) {
|
|||
.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} header_name the header name to fix
|
||||
* @returns {string} the fixed header name
|
||||
*/
|
||||
function fix_header_name_case(header_name) {
|
||||
return header_name
|
||||
function convertDashDelimitedTextToSnakeCase(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/^(.)|-(.)/g, match => match.toUpperCase());
|
||||
.replace(/^(.)|-(.)/g, (match) => match.toUpperCase());
|
||||
}
|
||||
|
||||
class Message {
|
||||
/**
|
||||
* @typedef {Object} MessageHeaders
|
||||
* @property {string?} content-type
|
||||
* @property {string} [subject]
|
||||
* @property {string} [text]
|
||||
* @property {MessageAttachment} [attachment]
|
||||
* @param {MessageHeaders} headers hash of message headers
|
||||
*/
|
||||
constructor(headers) {
|
||||
this.attachments = [];
|
||||
export class Message {
|
||||
public readonly attachments: MessageAttachment[] = [];
|
||||
public readonly header: Partial<MessageHeaders> = {
|
||||
'message-id': `<${new Date().getTime()}.${counter++}.${
|
||||
process.pid
|
||||
}@${hostname()}>`,
|
||||
date: getRFC2822Date(),
|
||||
};
|
||||
public readonly content: string = 'text/plain; charset=utf-8';
|
||||
public readonly text?: string;
|
||||
public alternative: AlternateMessageAttachment | null = null;
|
||||
|
||||
/**
|
||||
* @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';
|
||||
constructor(headers: Partial<MessageHeaders>) {
|
||||
for (const header in headers) {
|
||||
// allow user to override default content-type to override charset or send a single non-text message
|
||||
if (/^content-type$/i.test(header)) {
|
||||
this.content = headers[header];
|
||||
this.content = headers[header] as string;
|
||||
} else if (header === 'text') {
|
||||
this.text = headers[header];
|
||||
this.text = headers[header] as string;
|
||||
} else if (
|
||||
header === 'attachment' &&
|
||||
typeof headers[header] === 'object'
|
||||
|
@ -108,13 +132,15 @@ class Message {
|
|||
for (let i = 0; i < attachment.length; i++) {
|
||||
this.attach(attachment[i]);
|
||||
}
|
||||
} else {
|
||||
} else if (attachment != null) {
|
||||
this.attach(attachment);
|
||||
}
|
||||
} else if (header === 'subject') {
|
||||
this.header.subject = mimeWordEncode(headers.subject);
|
||||
} 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 {
|
||||
// allow any headers the user wants to set??
|
||||
// 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
|
||||
* @returns {Message} the current instance for chaining
|
||||
*/
|
||||
attach(options) {
|
||||
/*
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
public attach(options: MessageAttachment): Message {
|
||||
// sender can specify an attachment as an alternative
|
||||
if (options.alternative) {
|
||||
this.alternative = options;
|
||||
|
@ -160,7 +175,7 @@ class Message {
|
|||
* @param {string} [charset='utf-8'] the charset to encode as
|
||||
* @returns {Message} the current Message instance
|
||||
*/
|
||||
attach_alternative(html, charset) {
|
||||
attach_alternative(html: string, charset: string): Message {
|
||||
this.alternative = {
|
||||
data: html,
|
||||
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.
|
||||
* @returns {void}
|
||||
*/
|
||||
valid(callback) {
|
||||
public valid(callback: (arg0: boolean, arg1?: string) => void) {
|
||||
if (!this.header.from) {
|
||||
callback(false, 'message does not have a valid sender');
|
||||
}
|
||||
|
@ -185,9 +201,9 @@ class Message {
|
|||
} else if (this.attachments.length === 0) {
|
||||
callback(true, undefined);
|
||||
} else {
|
||||
const failed = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
this.attachments.forEach(attachment => {
|
||||
this.attachments.forEach((attachment) => {
|
||||
if (attachment.path) {
|
||||
if (fs.existsSync(attachment.path) == false) {
|
||||
failed.push(`${attachment.path} does not exist`);
|
||||
|
@ -206,132 +222,103 @@ class Message {
|
|||
}
|
||||
|
||||
/**
|
||||
* returns a stream of the current message
|
||||
* @returns {MessageStream} a stream of the current message
|
||||
* @public
|
||||
* @returns {*} a stream of the current message
|
||||
*/
|
||||
stream() {
|
||||
public stream() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
return new MessageStream(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(Error, string): void} callback the function to call with the error and buffer
|
||||
* @returns {void}
|
||||
*/
|
||||
read(callback) {
|
||||
public read(callback: (err: Error, buffer: string) => void) {
|
||||
let buffer = '';
|
||||
const str = this.stream();
|
||||
str.on('data', data => (buffer += data));
|
||||
str.on('end', err => callback(err, buffer));
|
||||
str.on('error', err => callback(err, buffer));
|
||||
str.on('data', (data) => (buffer += data));
|
||||
str.on('end', (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 {
|
||||
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();
|
||||
|
||||
/**
|
||||
* @type {Message}
|
||||
*/
|
||||
this.message = message;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.readable = true;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.paused = false;
|
||||
|
||||
/**
|
||||
* @type {Buffer}
|
||||
*/
|
||||
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.bufferIndex = 0;
|
||||
|
||||
/**
|
||||
* @param {string} [data] the data to output
|
||||
* @param {Function} [callback] the function
|
||||
* @param {any[]} [args] array of arguments to pass to the callback
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_mixed = () => {
|
||||
const boundary = generate_boundary();
|
||||
output(
|
||||
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||
);
|
||||
const output = (data: string) => {
|
||||
// can we buffer the data?
|
||||
if (this.buffer != null) {
|
||||
const bytes = Buffer.byteLength(data);
|
||||
|
||||
if (this.message.alternative == null) {
|
||||
output_text(this.message);
|
||||
output_message(boundary, this.message.attachments, 0, close);
|
||||
} else {
|
||||
const cb = () =>
|
||||
output_message(boundary, this.message.attachments, 0, close);
|
||||
output_alternative(this.message, cb);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} boundary the boundary text between outputs
|
||||
* @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}
|
||||
*/
|
||||
const output_message = (boundary, list, index, callback) => {
|
||||
if (index < list.length) {
|
||||
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)
|
||||
);
|
||||
if (bytes + this.bufferIndex < this.buffer.length) {
|
||||
this.buffer.write(data, this.bufferIndex);
|
||||
this.bufferIndex += bytes;
|
||||
}
|
||||
// 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;
|
||||
} 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}
|
||||
*/
|
||||
const output_attachment_headers = attachment => {
|
||||
let data = [];
|
||||
const headers = {
|
||||
const output_attachment_headers = (
|
||||
attachment: MessageAttachment | AlternateMessageAttachment
|
||||
) => {
|
||||
let data: string[] = [];
|
||||
const headers: Partial<MessageHeaders> = {
|
||||
'content-type':
|
||||
attachment.type +
|
||||
(attachment.charset ? `; charset=${attachment.charset}` : '') +
|
||||
|
@ -339,19 +326,23 @@ class MessageStream extends Stream {
|
|||
'content-transfer-encoding': 'base64',
|
||||
'content-disposition': attachment.inline
|
||||
? 'inline'
|
||||
: `attachment; filename="${mimeWordEncode(attachment.name)}"`,
|
||||
: `attachment; filename="${mimeWordEncode(
|
||||
attachment.name as string
|
||||
)}"`,
|
||||
};
|
||||
|
||||
// allow sender to override default headers
|
||||
for (const header in attachment.headers || {}) {
|
||||
headers[header.toLowerCase()] = attachment.headers[header];
|
||||
if (attachment.headers != null) {
|
||||
for (const header in attachment.headers) {
|
||||
headers[header.toLowerCase()] = attachment.headers[header];
|
||||
}
|
||||
}
|
||||
|
||||
for (const header in headers) {
|
||||
data = data.concat([
|
||||
fix_header_name_case(header),
|
||||
convertDashDelimitedTextToSnakeCase(header),
|
||||
': ',
|
||||
headers[header],
|
||||
headers[header] as string,
|
||||
CRLF,
|
||||
]);
|
||||
}
|
||||
|
@ -360,52 +351,38 @@ class MessageStream extends Stream {
|
|||
};
|
||||
|
||||
/**
|
||||
* @param {MessageAttachment} attachment the metadata to use as headers
|
||||
* @param {function(): void} callback the function to call after output is finished
|
||||
* @param {string} data the data to output as base64
|
||||
* @param {function(): void} [callback] the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_attachment = (attachment, callback) => {
|
||||
const build = attachment.path
|
||||
? output_file
|
||||
: attachment.stream
|
||||
? output_stream
|
||||
: output_data;
|
||||
output_attachment_headers(attachment);
|
||||
build(attachment, callback);
|
||||
const output_base64 = (data: string, callback?: () => void) => {
|
||||
const loops = Math.ceil(data.length / MIMECHUNK);
|
||||
let loop = 0;
|
||||
while (loop < loops) {
|
||||
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
|
||||
loop++;
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @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, 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 output_file = (
|
||||
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||
next: (err: NodeJS.ErrnoException | null) => void
|
||||
) => {
|
||||
const chunk = MIME64CHUNK * 16;
|
||||
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 {number} fd the file descriptor
|
||||
* @returns {void}
|
||||
*/
|
||||
const opened = (err, fd) => {
|
||||
const opened = (err: NodeJS.ErrnoException | null, fd: number) => {
|
||||
if (!err) {
|
||||
const read = (err, bytes) => {
|
||||
const read = (err: NodeJS.ErrnoException | null, bytes: number) => {
|
||||
if (!err && this.readable) {
|
||||
let encoding =
|
||||
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
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_stream = (attachment, callback) => {
|
||||
if (attachment.stream.readable) {
|
||||
const output_stream = (
|
||||
attachment: MessageAttachment | AlternateMessageAttachment,
|
||||
callback: () => void
|
||||
) => {
|
||||
if (attachment.stream != null && attachment.stream.readable) {
|
||||
let previous = Buffer.alloc(0);
|
||||
|
||||
attachment.stream.resume();
|
||||
|
||||
attachment.stream.on('end', () => {
|
||||
output_base64(previous.toString('base64'), callback);
|
||||
this.removeListener('pause', attachment.stream.pause);
|
||||
this.removeListener('resume', attachment.stream.resume);
|
||||
this.removeListener('error', attachment.stream.resume);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.removeListener('pause', attachment.stream!.pause);
|
||||
// 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?
|
||||
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 {function(): void} [callback] the function to call after output is finished
|
||||
* @param {string} boundary the boundary text between outputs
|
||||
* @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}
|
||||
*/
|
||||
const output_base64 = (data, callback) => {
|
||||
const loops = Math.ceil(data.length / MIMECHUNK);
|
||||
let loop = 0;
|
||||
while (loop < loops) {
|
||||
output(data.substring(MIMECHUNK * loop, MIMECHUNK * (loop + 1)) + CRLF);
|
||||
loop++;
|
||||
}
|
||||
if (callback) {
|
||||
const output_message = (
|
||||
boundary: string,
|
||||
list: MessageAttachment[],
|
||||
index: number,
|
||||
callback: () => void
|
||||
) => {
|
||||
if (index < list.length) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_text = message => {
|
||||
let data = [];
|
||||
const output_text = (message: Message) => {
|
||||
let data: string[] = [];
|
||||
|
||||
data = data.concat([
|
||||
'Content-Type:',
|
||||
|
@ -529,12 +573,36 @@ class MessageStream extends Stream {
|
|||
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 {function(): void} callback the function to call after output is finished
|
||||
* @returns {void}
|
||||
*/
|
||||
const output_alternative = (message, callback) => {
|
||||
const output_alternative = (
|
||||
message: Message & { alternative: AlternateMessageAttachment },
|
||||
callback: () => void
|
||||
) => {
|
||||
const boundary = generate_boundary();
|
||||
output(
|
||||
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
|
||||
|
@ -557,22 +625,24 @@ class MessageStream extends Stream {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @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, callback) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
const close = (err?: Error) => {
|
||||
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');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -593,18 +663,18 @@ class MessageStream extends Stream {
|
|||
* @returns {void}
|
||||
*/
|
||||
const output_header = () => {
|
||||
let data = [];
|
||||
let data: string[] = [];
|
||||
|
||||
for (const header in this.message.header) {
|
||||
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
|
||||
if (
|
||||
!/bcc/i.test(header) &&
|
||||
this.message.header.hasOwnProperty(header)
|
||||
Object.prototype.hasOwnProperty.call(this.message.header, header)
|
||||
) {
|
||||
data = data.concat([
|
||||
fix_header_name_case(header),
|
||||
convertDashDelimitedTextToSnakeCase(header),
|
||||
': ',
|
||||
this.message.header[header],
|
||||
this.message.header[header] as string,
|
||||
CRLF,
|
||||
]);
|
||||
}
|
||||
|
@ -614,109 +684,36 @@ class MessageStream extends Stream {
|
|||
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);
|
||||
process.nextTick(output_header);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* pause the stream
|
||||
* @returns {void}
|
||||
*/
|
||||
pause() {
|
||||
public pause() {
|
||||
this.paused = true;
|
||||
this.emit('pause');
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* resume the stream
|
||||
* @returns {void}
|
||||
*/
|
||||
resume() {
|
||||
public resume() {
|
||||
this.paused = false;
|
||||
this.emit('resume');
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* destroy the stream
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
public destroy() {
|
||||
this.emit(
|
||||
'destroy',
|
||||
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
|
||||
|
@ -724,14 +721,11 @@ class MessageStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* destroy the stream at first opportunity
|
||||
* @returns {void}
|
||||
*/
|
||||
destroySoon() {
|
||||
public destroySoon() {
|
||||
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');
|
||||
const { createHmac } = require('crypto');
|
||||
const { hostname } = require('os');
|
||||
const { connect, createSecureContext, TLSSocket } = require('tls');
|
||||
const { EventEmitter } = require('events');
|
||||
import { Socket } from 'net';
|
||||
import { createHmac } from 'crypto';
|
||||
import { hostname } from 'os';
|
||||
import { connect, createSecureContext, TLSSocket } from 'tls';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const SMTPResponse = require('./response');
|
||||
const SMTPError = require('./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';
|
||||
import { SMTPResponse } from './response';
|
||||
import { makeSMTPError, SMTPErrorStates } from './error';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum
|
||||
*/
|
||||
const AUTH_METHODS = {
|
||||
PLAIN: /** @type {'PLAIN'} */ ('PLAIN'),
|
||||
CRAM_MD5: /** @type {'CRAM-MD5'} */ ('CRAM-MD5'),
|
||||
LOGIN: /** @type {'LOGIN'} */ ('LOGIN'),
|
||||
XOAUTH2: /** @type {'XOAUTH2'} */ ('XOAUTH2'),
|
||||
};
|
||||
export const AUTH_METHODS = {
|
||||
PLAIN: 'PLAIN',
|
||||
'CRAM-MD5': 'CRAM-MD5',
|
||||
LOGIN: 'LOGIN',
|
||||
XOAUTH2: 'XOAUTH2',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum
|
||||
*/
|
||||
const SMTPState = {
|
||||
NOTCONNECTED: /** @type {0} */ (0),
|
||||
CONNECTING: /** @type {1} */ (1),
|
||||
CONNECTED: /** @type {2} */ (2),
|
||||
};
|
||||
export const SMTPState = {
|
||||
NOTCONNECTED: 0,
|
||||
CONNECTING: 1,
|
||||
CONNECTED: 2,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @type {0 | 1}
|
||||
*/
|
||||
let DEBUG = 0;
|
||||
export const DEFAULT_TIMEOUT = 5000 as const;
|
||||
|
||||
const SMTP_PORT = 25 as const;
|
||||
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
|
||||
* @returns {void}
|
||||
*/
|
||||
const log = (...args) => {
|
||||
const log = (...args: any[]) => {
|
||||
if (DEBUG === 1) {
|
||||
args.forEach(d =>
|
||||
args.forEach((d) =>
|
||||
console.log(
|
||||
typeof d === 'object'
|
||||
? d instanceof Error
|
||||
|
@ -86,35 +60,63 @@ const log = (...args) => {
|
|||
* @param {...*} args the arguments to apply to the function
|
||||
* @returns {void}
|
||||
*/
|
||||
const caller = (callback, ...args) => {
|
||||
const caller = (callback?: (...rest: any[]) => void, ...args: any[]) => {
|
||||
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
|
||||
*
|
||||
* @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({
|
||||
timeout,
|
||||
|
@ -127,146 +129,98 @@ class SMTP extends EventEmitter {
|
|||
tls,
|
||||
logger,
|
||||
authentication,
|
||||
} = {}) {
|
||||
}: Partial<SMTPConnectionOptions> = {}) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {0 | 1 | 2}
|
||||
*/
|
||||
this._state = SMTPState.NOTCONNECTED;
|
||||
if (Array.isArray(authentication)) {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._secure = false;
|
||||
if (typeof timeout === 'number') {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Socket|TLSSocket}
|
||||
*/
|
||||
this.sock = null;
|
||||
if (typeof domain === 'string') {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {{ [i: string]: string | boolean }}
|
||||
*/
|
||||
this.features = null;
|
||||
if (typeof host === 'string') {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 =
|
||||
if (
|
||||
ssl != null &&
|
||||
(typeof ssl === 'boolean' ||
|
||||
(typeof ssl === 'object' && Array.isArray(ssl) === false))
|
||||
? ssl
|
||||
: false;
|
||||
) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean|SMTPSocketOptions}
|
||||
*/
|
||||
this.tls =
|
||||
if (
|
||||
tls != null &&
|
||||
(typeof tls === 'boolean' ||
|
||||
(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);
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.loggedin = user && password ? false : true;
|
||||
|
||||
// keep these strings hidden when quicky debugging/logging
|
||||
this.user = /** @returns {string} */ () => user;
|
||||
this.password = /** @returns {string} */ () => password;
|
||||
this.user = () => user as string;
|
||||
this.password = () => password as string;
|
||||
|
||||
this.log = typeof logger === 'function' ? logger : log;
|
||||
if (typeof logger === 'function') {
|
||||
this.log = log;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {0 | 1} level -
|
||||
* @returns {void}
|
||||
*/
|
||||
debug(level) {
|
||||
public debug(level: 0 | 1) {
|
||||
DEBUG = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} the current state
|
||||
* @public
|
||||
* @returns {SMTPState} the current state
|
||||
*/
|
||||
state() {
|
||||
public state() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @returns {boolean} whether or not the instance is authorized
|
||||
*/
|
||||
authorized() {
|
||||
public authorized() {
|
||||
return this.loggedin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConnectOptions
|
||||
* @property {boolean} [ssl]
|
||||
*
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {number} [port] the port to use for the connection
|
||||
* @param {string} [host] the hostname to use for the connection
|
||||
* @param {ConnectOptions} [options={}] the options
|
||||
* @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.host = host;
|
||||
this.ssl = options.ssl || this.ssl;
|
||||
|
||||
if (this._state !== SMTPState.NOTCONNECTED) {
|
||||
this.quit(() =>
|
||||
this.connect(
|
||||
callback,
|
||||
port,
|
||||
host,
|
||||
options
|
||||
)
|
||||
);
|
||||
this.quit(() => this.connect(callback, port, host, options));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -285,9 +239,9 @@ class SMTP extends EventEmitter {
|
|||
this.close(true);
|
||||
caller(
|
||||
callback,
|
||||
SMTPError(
|
||||
makeSMTPError(
|
||||
'could not establish an ssl connection',
|
||||
SMTPError.CONNECTIONAUTH
|
||||
SMTPErrorStates.CONNECTIONAUTH
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
@ -300,7 +254,7 @@ class SMTP extends EventEmitter {
|
|||
* @param {Error} err err
|
||||
* @returns {void}
|
||||
*/
|
||||
const connectedErrBack = err => {
|
||||
const connectedErrBack = (err?: Error) => {
|
||||
if (!err) {
|
||||
connected();
|
||||
} else {
|
||||
|
@ -308,12 +262,19 @@ class SMTP extends EventEmitter {
|
|||
this.log(err);
|
||||
caller(
|
||||
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 (this._state === SMTPState.NOTCONNECTED && !this.sock) {
|
||||
return;
|
||||
|
@ -331,9 +292,9 @@ class SMTP extends EventEmitter {
|
|||
this.quit(() => {
|
||||
caller(
|
||||
callback,
|
||||
SMTPError(
|
||||
makeSMTPError(
|
||||
'bad response on connection',
|
||||
SMTPError.BADRESPONSE,
|
||||
SMTPErrorStates.BADRESPONSE,
|
||||
err,
|
||||
msg.data
|
||||
)
|
||||
|
@ -354,14 +315,10 @@ class SMTP extends EventEmitter {
|
|||
);
|
||||
} else {
|
||||
this.sock = new Socket();
|
||||
this.sock.connect(
|
||||
this.port,
|
||||
this.host,
|
||||
connectedErrBack
|
||||
);
|
||||
this.sock.connect(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.sock.once('response', response);
|
||||
|
@ -369,11 +326,12 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {string} str the string to send
|
||||
* @param {*} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
send(str, callback) {
|
||||
public send(str: string, callback: (...args: any[]) => void) {
|
||||
if (this.sock && this._state === SMTPState.CONNECTED) {
|
||||
this.log(str);
|
||||
|
||||
|
@ -390,25 +348,36 @@ class SMTP extends EventEmitter {
|
|||
this.close(true);
|
||||
caller(
|
||||
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 {function(...*): void} callback function to call after response
|
||||
* @param {(number[] | number)} [codes=[250]] array codes
|
||||
* @returns {void}
|
||||
*/
|
||||
command(cmd, callback, codes = [250]) {
|
||||
public command(
|
||||
cmd: string,
|
||||
callback: (...rest: any[]) => void,
|
||||
codes: number[] | number = [250]
|
||||
) {
|
||||
const codesArray = Array.isArray(codes)
|
||||
? codes
|
||||
: typeof codes === 'number'
|
||||
? [codes]
|
||||
: [250];
|
||||
? [codes]
|
||||
: [250];
|
||||
|
||||
const response = (err, msg) => {
|
||||
const response = (
|
||||
err: Error | null | undefined,
|
||||
msg: { code: string | number; data: string; message: string }
|
||||
) => {
|
||||
if (err) {
|
||||
caller(callback, err);
|
||||
} else {
|
||||
|
@ -421,7 +390,12 @@ class SMTP extends EventEmitter {
|
|||
}'${suffix}`;
|
||||
caller(
|
||||
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
|
||||
* host.
|
||||
|
@ -440,7 +415,7 @@ class SMTP extends EventEmitter {
|
|||
* @param {string} domain the domain to associate with the 'helo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
helo(callback, domain) {
|
||||
public helo(callback: (...rest: any[]) => void, domain?: string) {
|
||||
this.command(`helo ${domain || this.domain}`, (err, data) => {
|
||||
if (err) {
|
||||
caller(callback, err);
|
||||
|
@ -452,11 +427,16 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
starttls(callback) {
|
||||
const response = (err, msg) => {
|
||||
public starttls(callback: (...rest: any[]) => void) {
|
||||
const response = (err: Error, msg: { data: unknown }) => {
|
||||
if (this.sock == null) {
|
||||
throw new Error('null socket');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
err.message += ' while establishing a starttls session';
|
||||
caller(callback, err);
|
||||
|
@ -466,7 +446,7 @@ class SMTP extends EventEmitter {
|
|||
);
|
||||
const secureSocket = new TLSSocket(this.sock, { secureContext });
|
||||
|
||||
secureSocket.on('error', err => {
|
||||
secureSocket.on('error', (err: Error) => {
|
||||
this.close(true);
|
||||
caller(callback, err);
|
||||
});
|
||||
|
@ -474,7 +454,7 @@ class SMTP extends EventEmitter {
|
|||
this._secure = true;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
@ -483,15 +463,16 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {string} data the string to parse for features
|
||||
* @returns {void}
|
||||
*/
|
||||
parse_smtp_features(data) {
|
||||
public parse_smtp_features(data: string) {
|
||||
// According to RFC1869 some (badly written)
|
||||
// MTA's will disconnect on an ehlo. Toss an exception if
|
||||
// that happens -ddm
|
||||
|
||||
data.split('\n').forEach(ext => {
|
||||
data.split('\n').forEach((ext) => {
|
||||
const parse = ext.match(/^(?:\d+[-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
|
||||
|
||||
// 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
|
||||
// support using the old style.
|
||||
|
||||
if (parse != null) {
|
||||
if (parse != null && this.features != null) {
|
||||
// RFC 1869 requires a space between ehlo keyword and parameters.
|
||||
// It's actually stricter, in that only spaces are allowed between
|
||||
// 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 {string} domain the domain to associate with the 'ehlo' request
|
||||
* @returns {void}
|
||||
*/
|
||||
ehlo(callback, domain) {
|
||||
public ehlo(callback: (...rest: any[]) => void, domain?: string) {
|
||||
this.features = {};
|
||||
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
|
||||
if (err) {
|
||||
|
@ -534,106 +516,116 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {string} opt the features keyname to check
|
||||
* @returns {boolean} whether the extension exists
|
||||
*/
|
||||
has_extn(opt) {
|
||||
return this.features[opt.toLowerCase()] === undefined;
|
||||
public has_extn(opt: string): boolean {
|
||||
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 {string} domain the domain to associate with the 'help' request
|
||||
* @returns {void}
|
||||
*/
|
||||
help(callback, domain) {
|
||||
public help(callback: (...rest: any[]) => void, domain: string) {
|
||||
this.command(domain ? `help ${domain}` : 'help', callback, [211, 214]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
rset(callback) {
|
||||
public rset(callback: (...rest: any[]) => void) {
|
||||
this.command('rset', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
noop(callback) {
|
||||
public noop(callback: (...rest: any[]) => void) {
|
||||
this.send('noop', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {string} from the sender
|
||||
* @returns {void}
|
||||
*/
|
||||
mail(callback, from) {
|
||||
public mail(callback: (...rest: any[]) => void, from: string) {
|
||||
this.command(`mail FROM:${from}`, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @param {string} to the receiver
|
||||
* @returns {void}
|
||||
*/
|
||||
rcpt(callback, to) {
|
||||
public rcpt(callback: (...rest: any[]) => void, to: string) {
|
||||
this.command(`RCPT TO:${to}`, callback, [250, 251]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
data(callback) {
|
||||
public data(callback: (...rest: any[]) => void) {
|
||||
this.command('data', callback, [354]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
data_end(callback) {
|
||||
public data_end(callback: (...rest: any[]) => void) {
|
||||
this.command(`${CRLF}.`, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {string} data the message to send
|
||||
* @returns {void}
|
||||
*/
|
||||
message(data) {
|
||||
public message(data: string) {
|
||||
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 {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
verify(address, callback) {
|
||||
public verify(address: string, callback: (...rest: any[]) => void) {
|
||||
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 {function(...*): void} callback function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
expn(address, callback) {
|
||||
public expn(address: string, callback: (...rest: any[]) => void) {
|
||||
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
|
||||
* method tries ESMTP EHLO first.
|
||||
|
@ -642,10 +634,14 @@ class SMTP extends EventEmitter {
|
|||
* @param {string} [domain] the domain to associate with the command
|
||||
* @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...?
|
||||
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) => {
|
||||
if (err) {
|
||||
this.helo(response, domain);
|
||||
|
@ -657,6 +653,8 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Log in on an SMTP server that requires authentication.
|
||||
*
|
||||
* 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
|
||||
* @returns {void}
|
||||
*/
|
||||
login(callback, user, password, options) {
|
||||
public login(
|
||||
callback: (...rest: any[]) => void,
|
||||
user?: string,
|
||||
password?: string,
|
||||
options: { method?: string; domain?: string } = {}
|
||||
) {
|
||||
const login = {
|
||||
user: user ? () => user : this.user,
|
||||
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) {
|
||||
caller(callback, err);
|
||||
return;
|
||||
}
|
||||
|
||||
let method = null;
|
||||
let method: keyof typeof AUTH_METHODS | null = null;
|
||||
|
||||
/**
|
||||
* @param {string} challenge challenge
|
||||
* @returns {string} base64 cram hash
|
||||
*/
|
||||
const encode_cram_md5 = challenge => {
|
||||
const encode_cram_md5 = (challenge: string): string => {
|
||||
const hmac = createHmac('md5', login.password());
|
||||
hmac.update(Buffer.from(challenge, 'base64').toString('ascii'));
|
||||
return Buffer.from(`${login.user()} ${hmac.digest('hex')}`).toString(
|
||||
|
@ -702,7 +705,7 @@ class SMTP extends EventEmitter {
|
|||
/**
|
||||
* @returns {string} base64 login/password
|
||||
*/
|
||||
const encode_plain = () =>
|
||||
const encode_plain = (): string =>
|
||||
Buffer.from(`\u0000${login.user()}\u0000${login.password()}`).toString(
|
||||
'base64'
|
||||
);
|
||||
|
@ -711,7 +714,7 @@ class SMTP extends EventEmitter {
|
|||
* @see https://developers.google.com/gmail/xoauth2_protocol
|
||||
* @returns {string} base64 xoauth2 auth token
|
||||
*/
|
||||
const encode_xoauth2 = () =>
|
||||
const encode_xoauth2 = (): string =>
|
||||
Buffer.from(
|
||||
`user=${login.user()}\u0001auth=Bearer ${login.password()}\u0001\u0001`
|
||||
).toString('base64');
|
||||
|
@ -722,10 +725,8 @@ class SMTP extends EventEmitter {
|
|||
const preferred = this.authentication;
|
||||
let auth = '';
|
||||
|
||||
if (this.features && this.features.auth) {
|
||||
if (typeof this.features.auth === 'string') {
|
||||
auth = this.features.auth;
|
||||
}
|
||||
if (typeof this.features?.auth === 'string') {
|
||||
auth = this.features.auth;
|
||||
}
|
||||
|
||||
for (let i = 0; i < preferred.length; i++) {
|
||||
|
@ -742,12 +743,17 @@ class SMTP extends EventEmitter {
|
|||
* @param {*} data data
|
||||
* @returns {void}
|
||||
*/
|
||||
const failed = (err, data) => {
|
||||
const failed = (err: Error, data: unknown) => {
|
||||
this.loggedin = false;
|
||||
this.close(); // if auth is bad, close the connection, it won't get better by itself
|
||||
caller(
|
||||
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
|
||||
* @returns {void}
|
||||
*/
|
||||
const response = (err, data) => {
|
||||
const response = (err: Error | null | undefined, data: unknown) => {
|
||||
if (err) {
|
||||
failed(err, data);
|
||||
} else {
|
||||
|
@ -771,11 +777,15 @@ class SMTP extends EventEmitter {
|
|||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
const attempt = (err, data, msg) => {
|
||||
const attempt = (
|
||||
err: Error | null | undefined,
|
||||
data: unknown,
|
||||
msg: string
|
||||
) => {
|
||||
if (err) {
|
||||
failed(err, data);
|
||||
} else {
|
||||
if (method === AUTH_METHODS.CRAM_MD5) {
|
||||
if (method === AUTH_METHODS['CRAM-MD5']) {
|
||||
this.command(encode_cram_md5(msg), response, [235, 503]);
|
||||
} else if (method === AUTH_METHODS.LOGIN) {
|
||||
this.command(
|
||||
|
@ -793,7 +803,7 @@ class SMTP extends EventEmitter {
|
|||
* @param {string} msg msg
|
||||
* @returns {void}
|
||||
*/
|
||||
const attempt_user = (err, data, msg) => {
|
||||
const attempt_user = (err: Error, data: unknown) => {
|
||||
if (err) {
|
||||
failed(err, data);
|
||||
} else {
|
||||
|
@ -808,8 +818,8 @@ class SMTP extends EventEmitter {
|
|||
};
|
||||
|
||||
switch (method) {
|
||||
case AUTH_METHODS.CRAM_MD5:
|
||||
this.command(`AUTH ${AUTH_METHODS.CRAM_MD5}`, attempt, [334]);
|
||||
case AUTH_METHODS['CRAM-MD5']:
|
||||
this.command(`AUTH ${AUTH_METHODS['CRAM-MD5']}`, attempt, [334]);
|
||||
break;
|
||||
case AUTH_METHODS.LOGIN:
|
||||
this.command(`AUTH ${AUTH_METHODS.LOGIN}`, attempt_user, [334]);
|
||||
|
@ -830,7 +840,12 @@ class SMTP extends EventEmitter {
|
|||
break;
|
||||
default:
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
@ -840,10 +855,11 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {boolean} [force=false] whether or not to force destroy the connection
|
||||
* @returns {void}
|
||||
*/
|
||||
close(force = false) {
|
||||
public close(force = false) {
|
||||
if (this.sock) {
|
||||
if (force) {
|
||||
this.log('smtp connection destroyed!');
|
||||
|
@ -867,10 +883,11 @@ class SMTP extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {function(...*): void} [callback] function to call after response
|
||||
* @returns {void}
|
||||
*/
|
||||
quit(callback) {
|
||||
public quit(callback?: (...rest: any[]) => void) {
|
||||
this.command(
|
||||
'quit',
|
||||
(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