@terranlabs/email-nodes
Version:
Node-RED nodes to send and receive simple emails.
180 lines (170 loc) • 4.8 kB
JavaScript
import { Socket } from 'net';
import _tls from 'tls';
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import {
CRLF,
CRLF_BUFFER,
TERMINATOR_BUFFER,
TERMINATOR_BUFFER_ARRAY,
MULTI_LINE_COMMAND_NAME
} from './constant.mjs';
class Pop3Connection extends EventEmitter {
constructor({
host,
port,
tls,
timeout,
tlsOptions,
servername
}) {
super();
this.host = host;
this.port = port || (tls ? 995 : 110);
this.tls = tls;
this.timeout = timeout;
this._socket = null;
this._stream = null;
this._command;
this.tlsOptions = tlsOptions || {};
this.servername = servername || host;
}
_updateStream() {
this._stream = new Readable({
read: () => {},
});
return this._stream;
}
_pushStream(buffer) {
if (TERMINATOR_BUFFER_ARRAY.some((_buffer) => _buffer.equals(buffer))) {
return this._endStream();
}
const endBuffer = buffer.slice(-5);
if (endBuffer.equals(TERMINATOR_BUFFER)) {
this._stream.push(buffer.slice(0, -5));
return this._endStream();
}
this._stream.push(buffer);
}
_endStream(err) {
if (this._stream) {
this._stream.push(null);
}
this._stream = null;
this.emit('end', err);
}
connect() {
const { host, port, tlsOptions, servername } = this;
const socket = new Socket();
socket.setKeepAlive(true);
return new Promise((resolve, reject) => {
if (typeof this.timeout !== 'undefined') {
socket.setTimeout(this.timeout, () => {
const err = new Error('timeout');
err.eventName = 'timeout';
reject(err);
if (this.listeners('end').length) {
this.emit('end', err);
}
if (this.listeners('error').length) {
this.emit('error', err);
}
this._socket.end();
this._socket = null;
});
}
if (this.tls) {
const options = Object.assign({ host, port, socket, servername }, tlsOptions);
this._socket = _tls.connect(options);
} else {
this._socket = socket;
}
this._socket.on('data', (buffer) => {
if (this._stream) {
return this._pushStream(buffer);
}
if (buffer[0] === 45) { // '-'
const err = new Error(buffer.slice(5, -2));
err.eventName = 'error';
err.command = this._command;
return this.emit('error', err);
}
if (buffer[0] === 43) { // '+'
const firstLineEndIndex = buffer.indexOf(CRLF_BUFFER);
const infoBuffer = buffer.slice(4, firstLineEndIndex);
const [commandName] = (this._command || '').split(' ');
let stream = null;
if (MULTI_LINE_COMMAND_NAME.includes(commandName)) {
this._updateStream();
stream = this._stream;
const bodyBuffer = buffer.slice(firstLineEndIndex + 2);
if (bodyBuffer[0]) {
this._pushStream(bodyBuffer);
}
}
this.emit('response', infoBuffer.toString(), stream);
resolve();
return;
}
const err = new Error('Unexpected response');
err.eventName = 'bad-server-response';
reject(err);
});
this._socket.on('error', (err) => {
err.eventName = 'error';
if (this._stream) {
this.emit('error', err);
return;
}
reject(err);
this._socket = null;
});
this._socket.once('close', () => {
const err = new Error('close');
err.eventName = 'close';
reject(err);
this._socket = null;
});
this._socket.once('end', () => {
const err = new Error('end');
err.eventName = 'end';
reject(err);
this._socket = null;
});
socket.connect({
host,
port,
});
});
}
async command(...args) {
this._command = args.join(' ');
if (!this._socket) {
throw new Error('no-socket');
}
await new Promise((resolve, reject) => {
if (!this._stream) {
return resolve();
}
this.once('error', (err) => {
return reject(err);
});
this.once('end', (err) => {
return err ? reject(err) : resolve();
});
});
return new Promise((resolve, reject) => {
const rejectFn = (err) => reject(err);
this.once('error', rejectFn);
this.once('response', (info, stream) => {
this.removeListener('error', rejectFn);
resolve([info, stream]);
});
if (!this._socket) {
reject(new Error('no-socket'));
}
this._socket.write(`${this._command}${CRLF}`, 'utf8');
});
}
}
export default Pop3Connection;