smtp: complete typescript conversion

This commit is contained in:
Zack Schuster 2020-04-20 20:20:06 -07:00
parent 50b9ec18e8
commit 3bb741b3a8
9 changed files with 2079 additions and 352 deletions

View File

@ -1,7 +1,5 @@
import * as server from './smtp/client.js';
import * as message from './smtp/message.js';
import * as date from './smtp/date.js';
import * as SMTP from './smtp/smtp.js';
import * as error from './smtp/error.js';
export { server, message, date, SMTP, error };
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';

View File

@ -1,56 +1,72 @@
{
"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"
],
"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": {
"@ledge/configs": "22.0.2",
"@ledge/types": "6.1.0",
"@types/mailparser": "2.7.2",
"@types/smtp-server": "3.5.4",
"ava": "3.7.1",
"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",
"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",
"typescript": "3.8.3",
"ts-node": "8.9.0"
},
"engine": [
"node >= 10"
],
"main": "email.js",
"type": "module",
"scripts": {
"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
}
"test": "ava"
},
"license": "MIT",
"ava": {
"files": [
"test/*.ts"
],
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only"
]
},
"eslintIgnore": [
"rollup.config.js",
"rollup/email.bundle.js",
"email.esm.js"
],
"prettier": {
"singleQuote": true,
"trailingComma": "es5",
"useTabs": true
}
}

View File

@ -1,22 +1,23 @@
// @ts-ignore
import addressparser from 'addressparser';
import { Message, create, MessageAttachment } from './message.js';
import { SMTP, SMTPState } from './smtp.js';
import { Message, MessageHeaders, MessageAttachment } from './message';
import { SMTP, SMTPState, SMTPOptions } from './smtp';
export interface MessageStack {
callback: (error: Error, message: Message) => void;
callback: (error: Error | null, message: Message) => void;
message: Message;
attachment: MessageAttachment;
text: string;
returnPath: string;
from: string;
to: string | string[];
to: string | { address: string }[];
cc: string[];
bcc: string[];
}
class Client {
export class Client {
public smtp: SMTP;
public queue: any[];
public queue: MessageStack[] = []
public timer: any;
public sending: boolean;
public ready: boolean;
@ -44,15 +45,10 @@ class Client {
* @constructor
* @param {SMTPOptions} server smtp options
*/
constructor(server) {
constructor(server: Partial<SMTPOptions>) {
this.smtp = new SMTP(server);
//this.smtp.debug(1);
/**
* @type {MessageStack[]}
*/
this.queue = [];
/**
* @type {NodeJS.Timer}
*/
@ -69,27 +65,16 @@ class Client {
this.ready = false;
}
/**
* @param {Message|MessageStack} msg msg
* @param {function(Error, MessageStack): void} callback callback
* @returns {void}
*/
send(msg, callback) {
/**
* @type {Message}
*/
const message =
send(msg: Message, callback: (err: Error, msg: Message) => void): void {
const message: Message | null =
msg instanceof Message
? msg
: this._canMakeMessage(msg)
? create(msg)
? 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;
}
@ -100,7 +85,7 @@ class Client {
to: addressparser(message.header.to),
from: addressparser(message.header.from)[0].address,
callback: (callback || function() {}).bind(this),
};
} as MessageStack;
if (message.header.cc) {
stack.to = stack.to.concat(addressparser(message.header.cc));
@ -131,23 +116,23 @@ class Client {
* @private
* @returns {void}
*/
_poll() {
_poll(): void {
clearTimeout(this.timer);
if (this.queue.length) {
if (this.smtp.state() == SMTPState.NOTCONNECTED) {
if (this.queue.length > 0) {
if (this.smtp.state == SMTPState.NOTCONNECTED) {
this._connect(this.queue[0]);
} else if (
this.smtp.state() == SMTPState.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() == SMTPState.CONNECTED) {
else if (this.smtp.state == SMTPState.CONNECTED) {
this.timer = setTimeout(() => this.smtp.quit(), 1000);
}
}
@ -157,14 +142,14 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_connect(stack) {
_connect(stack: MessageStack): void {
/**
* @param {Error} err callback error
* @returns {void}
*/
const connect = err => {
const connect = (err: Error): void => {
if (!err) {
const begin = err => {
const begin = (err: Error) => {
if (!err) {
this.ready = true;
this._poll();
@ -177,7 +162,7 @@ class Client {
}
};
if (!this.smtp.authorized()) {
if (!this.smtp.isAuthorized) {
this.smtp.login(begin);
} else {
this.smtp.ehlo_or_helo_if_needed(begin);
@ -200,11 +185,11 @@ class Client {
* @param {MessageStack} msg message stack
* @returns {boolean} can make message
*/
_canMakeMessage(msg) {
return (
_canMakeMessage(msg: MessageHeaders): boolean {
return !!(
msg.from &&
(msg.to || msg.cc || msg.bcc) &&
(msg.text !== undefined || this._containsInlinedHtml(msg.attachment))
(msg.text != null || this._containsInlinedHtml(msg.attachment))
);
}
@ -213,7 +198,7 @@ class Client {
* @param {*} attachment attachment
* @returns {boolean} does contain
*/
_containsInlinedHtml(attachment) {
_containsInlinedHtml(attachment: any): boolean {
if (Array.isArray(attachment)) {
return attachment.some(att => {
return this._isAttachmentInlinedHtml(att);
@ -228,7 +213,7 @@ class Client {
* @param {*} attachment attachment
* @returns {boolean} is inlined
*/
_isAttachmentInlinedHtml(attachment) {
_isAttachmentInlinedHtml(attachment: any): boolean {
return (
attachment &&
(attachment.data || attachment.path) &&
@ -242,7 +227,7 @@ class Client {
* @param {function(MessageStack): void} next next
* @returns {function(Error): void} callback
*/
_sendsmtp(stack, next) {
_sendsmtp(stack: MessageStack, next: (msg: MessageStack) => void): (err: Error) => void {
/**
* @param {Error} [err] error
* @returns {void}
@ -263,7 +248,7 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_sendmail(stack) {
_sendmail(stack: MessageStack): void {
const from = stack.returnPath || stack.from;
this.sending = true;
this.smtp.mail(this._sendsmtp(stack, this._sendrcpt), '<' + from + '>');
@ -274,12 +259,12 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_sendrcpt(stack) {
_sendrcpt(stack: MessageStack): void {
if (stack.to == null || typeof stack.to === 'string') {
throw new TypeError('stack.to must be array');
}
const to = stack.to.shift().address;
const { address: to } = stack.to.shift() ?? {};
this.smtp.rcpt(
this._sendsmtp(stack, stack.to.length ? this._sendrcpt : this._senddata),
`<${to}>`
@ -291,7 +276,7 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_senddata(stack) {
_senddata(stack: MessageStack): void {
this.smtp.data(this._sendsmtp(stack, this._sendmessage));
}
@ -300,7 +285,7 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_sendmessage(stack) {
_sendmessage(stack: MessageStack): void {
const stream = stack.message.stream();
stream.on('data', data => this.smtp.message(data));
@ -324,15 +309,9 @@ class Client {
* @param {MessageStack} stack stack
* @returns {void}
*/
_senddone(err, stack) {
_senddone(err: Error | null, stack: MessageStack): void {
this.sending = false;
stack.callback(err, stack.message);
this._poll();
}
}
/**
* @param {SMTPOptions} server smtp options
* @returns {Client} the client
*/
export const connect = server => new Client(server);

View File

@ -1,7 +1,7 @@
/**
* @param {Date} [date] an optional date to convert to RFC2822 format
* @param {boolean} [useUtc=false] whether to parse the date as UTC (default: false)
* @returns {string} the converted date
* @param [date] an optional date to convert to RFC2822 format
* @param [useUtc] whether to parse the date as UTC (default: false)
* @returns the converted date
*/
export function getRFC2822Date(date = new Date(), useUtc = false) {
if (useUtc) {
@ -24,8 +24,8 @@ export function getRFC2822Date(date = new Date(), useUtc = false) {
}
/**
* @param {Date} [date] an optional date to convert to RFC2822 format (UTC)
* @returns {string} the converted date
* @param [date] an optional date to convert to RFC2822 format (UTC)
* @returns the converted date
*/
export function getRFC2822DateUTC(date = new Date()) {
const dates = date.toUTCString().split(' ');

View File

@ -2,10 +2,13 @@ import fs from 'fs';
import { hostname } from 'os';
import { Stream, Duplex } from 'stream';
// @ts-ignore
import addressparser from 'addressparser';
import emailjsMimeCodec from 'emailjs-mime-codec';
// @ts-ignore
import { mimeWordEncode } from 'emailjs-mime-codec';
import type { Indexed } from '@ledge/types';
import { getRFC2822Date } from './date.js';
import { getRFC2822Date } from './date';
const CRLF = '\r\n';
@ -25,22 +28,22 @@ export const MIME64CHUNK: 456 = (MIMECHUNK * 6) as 456;
export const BUFFERSIZE: 12768 = (MIMECHUNK * 24 * 7) as 12768;
export interface MessageAttachmentHeaders {
'content-type': string;
'content-transfer-encoding': string;
'content-disposition': string;
export interface MessageAttachmentHeaders extends Indexed {
'content-type'?: string;
'content-transfer-encoding'?: string;
'content-disposition'?: string;
}
export interface AlternateMessageAttachment {
export interface AlternateMessageAttachment extends Indexed {
headers: MessageAttachmentHeaders;
inline: boolean;
alternative: MessageAttachment;
related: MessageAttachment[];
alternative?: MessageAttachment;
related?: MessageAttachment[];
data: any;
encoded: any;
encoded?: any;
}
export interface MessageAttachment extends Partial<AlternateMessageAttachment> {
export interface MessageAttachment extends AlternateMessageAttachment {
name: string;
type: string;
charset: string;
@ -50,11 +53,17 @@ export interface MessageAttachment extends Partial<AlternateMessageAttachment> {
}
export interface MessageHeaders {
export interface MessageHeaders extends Indexed {
'content-type': string;
'message-id': string;
date: string;
from: string;
to: string;
cc: string;
bcc: string;
subject: string;
text: string;
attachment: MessageAttachment;
text: string | null;
attachment: MessageAttachment | MessageAttachment[];
}
let counter: number = 0;
@ -73,9 +82,9 @@ function generate_boundary() {
function convertPersonToAddress(person: string) {
return addressparser(person)
.map(({ name, address }) => {
.map(({ name, address }: { name: string, address: string }) => {
return name
? `${emailjsMimeCodec.mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
: address;
})
.join(', ');
@ -89,20 +98,12 @@ function convertDashDelimitedTextToSnakeCase(text: string) {
export class Message {
attachments: any[] = [];
alternative: Partial<MessageAttachment> | null = null;
header: {
'message-id': string;
date: string;
from?: string;
subject?: string;
to?: string;
cc?: string;
bcc?: string;
};
alternative: AlternateMessageAttachment | null = null;
header: Partial<MessageHeaders>;
content: string;
text: any;
constructor(headers: MessageHeaders) {
constructor(headers: Partial<MessageHeaders>) {
this.header = {
'message-id': `<${new Date().getTime()}.${counter++}.${
@ -118,20 +119,19 @@ export class Message {
this.content = headers[header];
} else if (header === 'text') {
this.text = headers[header];
} else if (
header === 'attachment' &&
typeof headers[header] === 'object'
) {
} else if (header === 'attachment') {
const attachment = headers[header];
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
if (attachment != null) {
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
}
} else {
this.attach(attachment);
}
} else {
this.attach(attachment);
}
} else if (header === 'subject') {
this.header.subject = emailjsMimeCodec.mimeWordEncode(headers.subject);
this.header.subject = mimeWordEncode(headers.subject);
} else if (/^(cc|bcc|to|from)/i.test(header)) {
this.header[header.toLowerCase()] = convertPersonToAddress(headers[header]);
} else {
@ -169,6 +169,9 @@ export class Message {
*/
attach_alternative(html: string, charset = 'utf-8'): Message {
this.alternative = {
headers: {
},
data: html,
charset,
type: 'text/html',
@ -182,7 +185,7 @@ export class Message {
* @param {function(boolean, string): void} callback This callback is displayed as part of the Requester class.
* @returns {void}
*/
valid(callback: (arg0: boolean, arg1: string) => void): void {
valid(callback: (arg0: boolean, arg1?: string) => void): void {
if (!this.header.from) {
callback(false, 'message does not have a valid sender');
}
@ -192,7 +195,7 @@ export class Message {
} else if (this.attachments.length === 0) {
callback(true, undefined);
} else {
const failed = [];
const failed: string[] = [];
this.attachments.forEach(attachment => {
if (attachment.path) {
@ -237,7 +240,7 @@ class MessageStream extends Stream {
message: Message;
readable: boolean;
paused: boolean;
buffer: Buffer;
buffer: Buffer | null;
bufferIndex: number;
/**
* @param {Message} message the message to stream
@ -283,9 +286,11 @@ class MessageStream extends Stream {
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);
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)
);
}
};
@ -315,12 +320,11 @@ class MessageStream extends Stream {
};
/**
* @param {MessageAttachment} attachment the metadata to use as headers
* @returns {void}
*/
const output_attachment_headers = (attachment: Partial<MessageAttachment>): void => {
let data = [];
const headers = {
const output_attachment_headers = (attachment: MessageAttachment | AlternateMessageAttachment): void => {
let data: string[] = [];
const headers: Partial<MessageHeaders> = {
'content-type':
attachment.type +
(attachment.charset ? `; charset=${attachment.charset}` : '') +
@ -328,7 +332,7 @@ class MessageStream extends Stream {
'content-transfer-encoding': 'base64',
'content-disposition': attachment.inline
? 'inline'
: `attachment; filename="${emailjsMimeCodec.mimeWordEncode(attachment.name)}"`,
: `attachment; filename="${mimeWordEncode(attachment.name)}"`,
};
// allow sender to override default headers
@ -348,12 +352,7 @@ class MessageStream extends Stream {
output(data.concat([CRLF]).join(''));
};
/**
* @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_attachment = (attachment: Partial<MessageAttachment>, callback: () => void): void => {
const output_attachment = (attachment: MessageAttachment | AlternateMessageAttachment, callback: () => void): void => {
const build = attachment.path
? output_file
: attachment.stream
@ -368,7 +367,7 @@ class MessageStream extends Stream {
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const output_data = (attachment: Partial<MessageAttachment>, callback: () => void): void => {
const output_data = (attachment: MessageAttachment | AlternateMessageAttachment, callback: () => void): void => {
output_base64(
attachment.encoded
? attachment.data
@ -377,15 +376,10 @@ class MessageStream extends Stream {
);
};
/**
* @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: Partial<MessageAttachment>, next: (arg0: NodeJS.ErrnoException) => void): void => {
const output_file = (attachment: MessageAttachment | AlternateMessageAttachment, next: (err: NodeJS.ErrnoException) => void): 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
@ -394,7 +388,7 @@ class MessageStream extends Stream {
*/
const opened = (err: Error, fd: number): void => {
if (!err) {
const read = (err, bytes) => {
const read = (err: Error, bytes: number) => {
if (!err && this.readable) {
let encoding =
attachment && attachment.headers
@ -441,20 +435,20 @@ class MessageStream extends Stream {
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const output_stream = (attachment: Partial<MessageAttachment>, callback: () => void): void => {
const output_stream = (attachment: MessageAttachment | AlternateMessageAttachment, callback: () => void): void => {
if (attachment.stream.readable) {
let previous = Buffer.alloc(0);
attachment.stream.resume();
attachment.stream.on('end', () => {
(attachment as MessageAttachment).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);
});
attachment.stream.on('data', buff => {
(attachment as MessageAttachment).stream.on('data', buff => {
// do we have bytes from a previous stream data event?
let buffer = Buffer.isBuffer(buff) ? buff : Buffer.from(buff);
@ -503,7 +497,7 @@ class MessageStream extends Stream {
* @returns {void}
*/
const output_text = (message: Message): void => {
let data = [];
let data: string[] = [];
data = data.concat([
'Content-Type:',
@ -523,7 +517,7 @@ class MessageStream extends Stream {
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const output_alternative = (message: Message, callback: () => void): void => {
const output_alternative = (message: Message & { alternative: AlternateMessageAttachment }, callback: () => void): void => {
const boundary = generate_boundary();
output(
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
@ -551,13 +545,13 @@ class MessageStream extends Stream {
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const output_related = (message: Partial<MessageAttachment>, callback: () => void): void => {
const output_related = (message: AlternateMessageAttachment, callback: () => void): 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_message(boundary, message.related ?? [], 0, () => {
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
callback();
});
@ -582,7 +576,7 @@ class MessageStream extends Stream {
* @returns {void}
*/
const output_header = (): void => {
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...
@ -608,51 +602,56 @@ class MessageStream extends Stream {
* @param [callback] the function
* @param [args] array of arguments to pass to the callback
*/
const output = (data: string, callback?: (...args: any[]) => void, args?: any[]) => {
const bytes = Buffer.byteLength(data);
const output = (data: string, callback?: (...args: any[]) => void, args: any[] = []) => {
// 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;
}
if (this.buffer != null) {
const bytes = Buffer.byteLength(data);
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) {
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 empty out the buffer, so let's wait till we resume before adding to it
}
// 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 {
this.once('resume', () => output(data, callback, args));
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 (typeof callback === 'function') {
if (this.paused) {
this.once('resume', () => callback.apply(null, args));
} else {
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));
}
}
}
};
@ -661,7 +660,7 @@ class MessageStream extends Stream {
if (err) {
this.emit('error', err);
} else {
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
this.emit('data', this.buffer?.toString('utf-8', 0, this.bufferIndex) ?? '');
this.emit('end');
}
this.buffer = null;
@ -715,8 +714,3 @@ class MessageStream extends Stream {
this.emit('destroy');
}
}
export /**
* @param {{ content: string; subject?: string; text?: string; attachment?: MessageAttachment; }} headers
*/
const create = headers => new Message(headers);

View File

@ -1,7 +1,7 @@
import { Socket } from 'net';
import { TLSSocket } from 'tls';
import { makeSMTPError, SMTPErrorStates } from './error.js';
import { makeSMTPError, SMTPErrorStates } from './error';
export class SMTPResponse {
private buffer = '';
@ -47,7 +47,7 @@ export class SMTPResponse {
.trim()
.split(/\n/)
.pop()
.match(/^(\d{3})\s/)
?.match(/^(\d{3})\s/) ?? false
) {
return;
}

View File

@ -4,8 +4,9 @@ import { hostname } from 'os';
import { connect, createSecureContext, TLSSocket } from 'tls';
import { EventEmitter } from 'events';
import { SMTPResponse, monitor } from './response.js';
import { makeSMTPError, SMTPErrorStates } from './error.js';
import { SMTPResponse, monitor } from './response';
import { makeSMTPError, SMTPErrorStates } from './error';
import { Indexed } from '@ledge/types';
/**
* @readonly
@ -38,15 +39,11 @@ const SMTP_TLS_PORT: 587 = 587;
*/
const CRLF: '\r\n' = '\r\n';
/**
* @readonly
* @enum
*/
export const AUTH_METHODS = {
PLAIN: /** @type {'PLAIN'} */ ('PLAIN'),
CRAM_MD5: /** @type {'CRAM-MD5'} */ ('CRAM-MD5'),
LOGIN: /** @type {'LOGIN'} */ ('LOGIN'),
XOAUTH2: /** @type {'XOAUTH2'} */ ('XOAUTH2'),
export enum AUTH_METHODS {
PLAIN = 'PLAIN',
CRAM_MD5 = 'CRAM-MD5',
LOGIN = 'LOGIN',
XOAUTH2 = 'XOAUTH2',
};
/**
@ -87,7 +84,7 @@ const log = (...args: any[]): void => {
* @param {...*} args the arguments to apply to the function
* @returns {void}
*/
const caller = (callback: (...rest: any[]) => void, ...args: any[]): void => {
const caller = (callback?: (...rest: any[]) => void, ...args: any[]): void => {
if (typeof callback === 'function') {
callback.apply(null, args);
}
@ -100,7 +97,7 @@ export interface SMTPSocketOptions {
}
export interface SMTPOptions {
timeout: number;
timeout: number | null;
user: string;
password: string;
domain: string;
@ -120,8 +117,9 @@ export class SMTP extends EventEmitter {
private _state: 0 | 1 | 2 = SMTPState.NOTCONNECTED;
private _isAuthorized = false;
private _isSecure = false;
private _user = '';
private _password = '';
private _user?: string = '';
private _password?: string = '';
private _timeout: number = TIMEOUT;
public set debug(level: 0 | 1) {
DEBUG = level;
@ -131,6 +129,10 @@ export class SMTP extends EventEmitter {
return this._state;
}
public get timeout() {
return this._timeout;
}
public get user() {
return this._user;
}
@ -144,10 +146,9 @@ export class SMTP extends EventEmitter {
}
protected sock: Socket | TLSSocket | null = null;
protected features: { [i: string]: string | boolean } | null = null;
protected features: Indexed<string | boolean> = {};
protected monitor: SMTPResponse | null = null;
protected authentication: any[];
protected timeout: number = TIMEOUT;
protected domain = hostname();
protected host = 'localhost';
protected ssl: boolean | SMTPSocketOptions = false;
@ -185,7 +186,7 @@ export class SMTP extends EventEmitter {
];
if (typeof timeout === 'number') {
this.timeout = timeout;
this._timeout = timeout;
}
if (typeof domain === 'string') {
@ -285,7 +286,7 @@ export class SMTP extends EventEmitter {
}
};
const response = (err, msg) => {
const response = (err: Error, msg: { code: string | number, data: string }) => {
if (err) {
if (this._state === SMTPState.NOTCONNECTED && !this.sock) {
return;
@ -333,7 +334,7 @@ export class SMTP extends EventEmitter {
);
}
this.monitor = monitor(this.sock, this.timeout, () =>
this.monitor = monitor(this.sock, this._timeout, () =>
this.close(true)
);
this.sock.once('response', response);
@ -380,7 +381,7 @@ export class SMTP extends EventEmitter {
? [codes]
: [250];
const response = (err, msg) => {
const response = (err: Error, msg: { code: string | number, data: string, message: string }) => {
if (err) {
caller(callback, err);
} else {
@ -393,7 +394,7 @@ export class SMTP extends EventEmitter {
}'${suffix}`;
caller(
callback,
makeSMTPError(errorMessage, SMTPErrorStates.BADRESPONSE, null, msg.data)
makeSMTPError(errorMessage, SMTPErrorStates.BADRESPONSE, undefined, msg.data)
);
}
}
@ -412,7 +413,7 @@ export class SMTP extends EventEmitter {
* @param {string} domain the domain to associate with the 'helo' request
* @returns {void}
*/
helo(callback: (...rest: any[]) => void, domain: string): void {
helo(callback: (...rest: any[]) => void, domain?: string): void {
this.command(`helo ${domain || this.domain}`, (err, data) => {
if (err) {
caller(callback, err);
@ -428,7 +429,11 @@ export class SMTP extends EventEmitter {
* @returns {void}
*/
starttls(callback: (...rest: any[]) => void): void {
const response = (err, msg) => {
const response = (err: Error, msg: { data: any }) => {
if (this.sock == null) {
throw new Error('null socket');
}
if (err) {
err.message += ' while establishing a starttls session';
caller(callback, err);
@ -438,7 +443,7 @@ export 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);
});
@ -446,7 +451,7 @@ export class SMTP extends EventEmitter {
this._isSecure = true;
this.sock = secureSocket;
monitor(this.sock, this.timeout, () => this.close(true));
monitor(this.sock, this._timeout, () => this.close(true));
caller(callback, msg.data);
}
};
@ -488,7 +493,7 @@ export class SMTP extends EventEmitter {
* @param {string} domain the domain to associate with the 'ehlo' request
* @returns {void}
*/
ehlo(callback: (...rest: any[]) => void, domain: string): void {
ehlo(callback: (...rest: any[]) => void, domain?: string): void {
this.features = {};
this.command(`ehlo ${domain || this.domain}`, (err, data) => {
if (err) {
@ -579,7 +584,7 @@ export class SMTP extends EventEmitter {
*/
message(data: string): void {
this.log(data);
this.sock.write(data);
this.sock?.write(data) ?? this.log('no socket to write to');
}
/**
@ -614,10 +619,10 @@ export class SMTP extends EventEmitter {
* @param {string} [domain] the domain to associate with the command
* @returns {void}
*/
ehlo_or_helo_if_needed(callback: (...rest: any[]) => void, domain: string): void {
ehlo_or_helo_if_needed(callback: (...rest: any[]) => void, domain?: string): void {
// is this code callable...?
if (!this.features) {
const response = (err, data) => caller(callback, err, data);
if (Object.keys(this.features).length === 0) {
const response = (err: Error, data: any) => caller(callback, err, data);
this.ehlo((err, data) => {
if (err) {
this.helo(response, domain);
@ -642,22 +647,22 @@ export class SMTP extends EventEmitter {
* @param {{ method: string, domain: string }} [options] login options
* @returns {void}
*/
login(callback: (...rest: any[]) => void, user: string, password: string, options: { method: string; domain: string; }): void {
login(callback: (...rest: any[]) => void, user = '', password = '', options: { method?: string; domain?: string; } = {}): void {
const login = {
user: () => user || this.user,
password: () => password || this.password,
user: () => user || this.user || '',
password: () => password || this.password || '',
method: options && options.method ? options.method.toUpperCase() : '',
};
const domain = options && options.domain ? options.domain : this.domain;
const initiate = (err, data) => {
const initiate = (err: Error, data: any) => {
if (err) {
caller(callback, err);
return;
}
let method = null;
let method: AUTH_METHODS | null = null;
/**
* @param {string} challenge challenge
@ -765,7 +770,7 @@ export class SMTP extends EventEmitter {
* @param {string} msg msg
* @returns {void}
*/
const attempt_user = (err: Error, data: any, msg: string): void => {
const attempt_user = (err: Error, data: any): void => {
if (err) {
failed(err, data);
} else {
@ -802,7 +807,7 @@ export class SMTP extends EventEmitter {
break;
default:
const msg = 'no form of authorization supported';
const err = makeSMTPError(msg, SMTPErrorStates.AUTHNOTSUPPORTED, null, data);
const err = makeSMTPError(msg, SMTPErrorStates.AUTHNOTSUPPORTED, undefined, data);
caller(callback, err);
break;
}
@ -834,7 +839,7 @@ export class SMTP extends EventEmitter {
this._state = SMTPState.NOTCONNECTED;
this._isSecure = false;
this.sock = null;
this.features = null;
this.features = {};
this._isAuthorized = !(this._user && this._password);
}
@ -842,7 +847,7 @@ export class SMTP extends EventEmitter {
* @param {function(...*): void} [callback] function to call after response
* @returns {void}
*/
quit(callback: (...rest: any[]) => void): void {
quit(callback?: (...rest: any[]) => void): void {
this.command(
'quit',
(err, data) => {

14
tsconfig.json Normal file
View File

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

1871
yarn.lock

File diff suppressed because it is too large Load Diff