emailjs/smtp/message.ts

736 lines
19 KiB
TypeScript
Raw Normal View History

import fs, { PathLike } from 'fs';
import { hostname } from 'os';
2020-04-23 04:26:49 +00:00
import { Stream } from 'stream';
import type { Readable } from 'stream';
2020-06-17 03:11:19 +00:00
import { addressparser } from './address';
2020-04-21 03:20:06 +00:00
import { getRFC2822Date } from './date';
import { mimeWordEncode } from './mime';
2018-06-24 01:33:53 +00:00
const CRLF = '\r\n' as const;
2018-06-28 02:56:26 +00:00
/**
* MIME standard wants 76 char chunks when sending out.
*/
export const MIMECHUNK = 76 as const;
2018-06-28 02:56:26 +00:00
/**
* meets both base64 and mime divisibility
*/
export const MIME64CHUNK = (MIMECHUNK * 6) as 456;
2018-06-28 02:56:26 +00:00
/**
* size of the message stream buffer
*/
export const BUFFERSIZE = (MIMECHUNK * 24 * 7) as 12768;
export interface MessageAttachmentHeaders {
2020-05-24 14:47:49 +00:00
[index: string]: string | undefined;
2020-04-21 03:20:06 +00:00
'content-type'?: string;
2020-05-24 14:47:49 +00:00
'content-transfer-encoding'?: BufferEncoding | '7bit' | '8bit';
2020-04-21 03:20:06 +00:00
'content-disposition'?: string;
}
export interface MessageAttachment {
2020-05-24 14:47:49 +00:00
[index: string]:
| string
| boolean
| MessageAttachment
| MessageAttachment[]
| MessageAttachmentHeaders
| Readable
2020-05-24 14:47:49 +00:00
| PathLike
| undefined;
name?: string;
headers?: MessageAttachmentHeaders;
inline?: boolean;
2020-05-01 20:29:07 +00:00
alternative?: MessageAttachment | boolean;
2020-04-21 03:20:06 +00:00
related?: MessageAttachment[];
data?: string;
2020-05-24 14:47:49 +00:00
encoded?: boolean;
stream?: Readable;
2020-05-24 14:47:49 +00:00
path?: PathLike;
type?: string;
charset?: string;
method?: string;
}
export interface MessageHeaders {
[index: string]:
| boolean
| string
| string[]
| null
| MessageAttachment
| MessageAttachment[];
'content-type': string;
2020-04-21 03:20:06 +00:00
'message-id': string;
2020-05-24 14:47:49 +00:00
'return-path': string | null;
2020-04-21 03:20:06 +00:00
date: string;
from: string | string[];
to: string | string[];
cc: string | string[];
bcc: string | string[];
subject: string;
2020-04-21 03:20:06 +00:00
text: string | null;
attachment: MessageAttachment | MessageAttachment[];
}
2020-05-24 14:47:49 +00:00
let counter = 0;
function generateBoundary() {
2018-05-27 04:25:08 +00:00
let text = '';
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
2011-02-23 21:23:37 +00:00
2018-05-27 04:25:08 +00:00
for (let i = 0; i < 69; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
2011-02-23 21:23:37 +00:00
2018-05-27 04:25:08 +00:00
return text;
}
function convertPersonToAddress(person: string | string[]) {
return addressparser(person)
.map(({ name, address }) => {
2018-05-27 04:25:08 +00:00
return name
2020-04-21 03:20:06 +00:00
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
2018-05-27 04:25:08 +00:00
: address;
})
.join(', ');
}
2011-02-23 21:23:37 +00:00
function convertDashDelimitedTextToSnakeCase(text: string) {
return text
2018-05-27 04:25:08 +00:00
.toLowerCase()
2020-04-23 04:26:49 +00:00
.replace(/^(.)|-(.)/g, (match) => match.toUpperCase());
2012-06-10 07:37:13 +00:00
}
export class Message {
2020-05-24 14:47:49 +00:00
public readonly attachments: MessageAttachment[] = [];
public readonly header: Partial<MessageHeaders> = {
'message-id': `<${new Date().getTime()}.${counter++}.${
process.pid
}@${hostname()}>`,
date: getRFC2822Date(),
};
2020-05-24 14:47:49 +00:00
public readonly content: string = 'text/plain; charset=utf-8';
public readonly text?: string;
public alternative: MessageAttachment | null = null;
2020-05-27 05:47:24 +00:00
/**
* Construct an rfc2822-compliant message object.
*
* Special notes:
* - The `from` field is required.
* - At least one `to`, `cc`, or `bcc` header is also required.
* - You can also add whatever other headers you want.
*
* @see https://tools.ietf.org/html/rfc2822
* @param {Partial<MessageHeaders>} headers Message headers
*/
2020-04-21 03:20:06 +00:00
constructor(headers: Partial<MessageHeaders>) {
for (const header in headers) {
2018-05-27 04:25:08 +00:00
// allow user to override default content-type to override charset or send a single non-text message
if (/^content-type$/i.test(header)) {
2020-05-24 14:47:49 +00:00
this.content = headers[header] as string;
2018-05-27 04:25:08 +00:00
} else if (header === 'text') {
2020-04-26 16:20:56 +00:00
this.text = headers[header] as string;
} else if (
header === 'attachment' &&
typeof headers[header] === 'object'
) {
2018-07-06 18:07:19 +00:00
const attachment = headers[header];
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
2018-05-27 04:25:08 +00:00
}
} else if (attachment != null) {
this.attach(attachment);
2018-05-27 04:25:08 +00:00
}
} else if (header === 'subject') {
this.header.subject = mimeWordEncode(headers.subject as string);
2018-05-27 04:25:08 +00:00
} else if (/^(cc|bcc|to|from)/i.test(header)) {
2020-04-23 04:26:49 +00:00
this.header[header.toLowerCase()] = convertPersonToAddress(
headers[header] as string | string[]
2020-04-23 04:26:49 +00:00
);
2018-05-27 04:25:08 +00:00
} else {
// allow any headers the user wants to set??
this.header[header.toLowerCase()] = headers[header];
}
}
}
2018-06-28 02:56:26 +00:00
/**
2020-05-26 07:42:19 +00:00
* Attach a file to the message.
*
* Can be called multiple times, each adding a new attachment.
*
2020-05-02 00:33:07 +00:00
* @public
2018-06-28 02:56:26 +00:00
* @param {MessageAttachment} options attachment options
* @returns {Message} the current instance for chaining
*/
2020-09-04 05:12:49 +00:00
public attach(options: MessageAttachment) {
2018-05-27 04:25:08 +00:00
// sender can specify an attachment as an alternative
if (options.alternative) {
this.alternative = options;
this.alternative.charset = options.charset || 'utf-8';
this.alternative.type = options.type || 'text/html';
this.alternative.inline = true;
} else {
this.attachments.push(options);
}
2018-05-27 04:25:08 +00:00
return this;
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
* @param {function(isValid: boolean, invalidReason: string): void} callback .
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
public valid(callback: (isValid: boolean, invalidReason?: string) => void) {
if (
typeof this.header.from !== 'string' &&
Array.isArray(this.header.from) === false
) {
2020-05-27 05:47:24 +00:00
callback(false, 'Message must have a `from` header');
} else if (
2020-05-27 05:47:24 +00:00
typeof this.header.to !== 'string' &&
Array.isArray(this.header.to) === false &&
2020-05-27 05:47:24 +00:00
typeof this.header.cc !== 'string' &&
Array.isArray(this.header.cc) === false &&
typeof this.header.bcc !== 'string' &&
Array.isArray(this.header.bcc) === false
2020-05-27 05:47:24 +00:00
) {
callback(
false,
'Message must have at least one `to`, `cc`, or `bcc` header'
);
2018-05-27 04:25:08 +00:00
} else if (this.attachments.length === 0) {
2018-06-29 00:55:20 +00:00
callback(true, undefined);
2018-05-27 04:25:08 +00:00
} else {
2020-04-21 03:20:06 +00:00
const failed: string[] = [];
2018-05-27 04:25:08 +00:00
2020-04-23 04:26:49 +00:00
this.attachments.forEach((attachment) => {
2018-05-27 04:25:08 +00:00
if (attachment.path) {
2018-06-25 02:28:30 +00:00
if (fs.existsSync(attachment.path) == false) {
2018-05-27 04:25:08 +00:00
failed.push(`${attachment.path} does not exist`);
2018-06-04 16:40:23 +00:00
}
2018-05-27 04:25:08 +00:00
} else if (attachment.stream) {
2018-06-04 16:40:23 +00:00
if (!attachment.stream.readable) {
2018-05-27 04:25:08 +00:00
failed.push('attachment stream is not readable');
2018-06-04 16:40:23 +00:00
}
2018-05-27 04:25:08 +00:00
} else if (!attachment.data) {
failed.push('attachment has no data associated with it');
}
});
callback(failed.length === 0, failed.join(', '));
}
2018-05-27 04:25:08 +00:00
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2020-09-04 15:48:12 +00:00
* @returns {MessageStream} a stream of the current message
2018-06-28 02:56:26 +00:00
*/
2020-05-02 00:33:07 +00:00
public stream() {
2018-05-27 04:25:08 +00:00
return new MessageStream(this);
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2018-06-29 03:44:54 +00:00
* @param {function(Error, string): void} callback the function to call with the error and buffer
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public read(callback: (err: Error, buffer: string) => void) {
2018-05-27 04:25:08 +00:00
let buffer = '';
const str = this.stream();
2020-04-23 04:26:49 +00:00
str.on('data', (data) => (buffer += data));
str.on('end', (err) => callback(err, buffer));
str.on('error', (err) => callback(err, buffer));
2018-05-27 04:25:08 +00:00
}
}
class MessageStream extends Stream {
readable = true;
paused = false;
buffer: Buffer | null = Buffer.alloc(MIMECHUNK * 24 * 7);
bufferIndex = 0;
2018-06-28 02:56:26 +00:00
/**
2020-09-04 15:48:12 +00:00
* @param {Message} message the message to stream
2018-06-28 02:56:26 +00:00
*/
constructor(private message: Message) {
super();
2018-06-28 02:56:26 +00:00
/**
2020-05-24 14:47:49 +00:00
* @param {string} [data] the data to output
* @param {Function} [callback] the function
* @param {any[]} [args] array of arguments to pass to the callback
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
2020-05-24 14:47:49 +00:00
const output = (data: string) => {
// can we buffer the data?
if (this.buffer != null) {
const bytes = Buffer.byteLength(data);
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));
}
2018-05-27 04:25:08 +00:00
}
}
};
2018-06-28 02:56:26 +00:00
/**
* @param {MessageAttachment} [attachment] the attachment whose headers you would like to output
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
const outputAttachmentHeaders = (attachment: MessageAttachment) => {
2020-04-21 03:20:06 +00:00
let data: string[] = [];
const headers: Partial<MessageHeaders> = {
2018-05-27 04:25:08 +00:00
'content-type':
attachment.type +
(attachment.charset ? `; charset=${attachment.charset}` : '') +
(attachment.method ? `; method=${attachment.method}` : ''),
'content-transfer-encoding': 'base64',
'content-disposition': attachment.inline
? 'inline'
2020-05-24 14:47:49 +00:00
: `attachment; filename="${mimeWordEncode(
attachment.name as string
)}"`,
};
2018-06-28 02:56:26 +00:00
// allow sender to override default headers
if (attachment.headers != null) {
for (const header in attachment.headers) {
headers[header.toLowerCase()] = attachment.headers[header];
}
2018-05-27 04:25:08 +00:00
}
for (const header in headers) {
2018-05-27 04:25:08 +00:00
data = data.concat([
convertDashDelimitedTextToSnakeCase(header),
2018-05-27 04:25:08 +00:00
': ',
2020-05-24 14:47:49 +00:00
headers[header] as string,
2018-05-27 04:25:08 +00:00
CRLF,
]);
}
output(data.concat([CRLF]).join(''));
};
2018-06-28 02:56:26 +00:00
/**
2020-05-24 14:47:49 +00:00
* @param {string} data the data to output as base64
* @param {function(): void} [callback] the function to call after output is finished
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
const outputBase64 = (data: string, callback?: () => void) => {
2020-05-24 14:47:49 +00:00
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();
}
2018-05-27 04:25:08 +00:00
};
const outputFile = (
attachment: MessageAttachment,
2020-05-01 16:25:32 +00:00
next: (err: NodeJS.ErrnoException | null) => void
) => {
2018-05-27 04:25:08 +00:00
const chunk = MIME64CHUNK * 16;
const buffer = Buffer.alloc(chunk);
2020-04-21 03:20:06 +00:00
const closed = (fd: number) => fs.closeSync(fd);
2018-05-27 04:25:08 +00:00
2018-06-28 02:56:26 +00:00
/**
2018-06-29 03:44:54 +00:00
* @param {Error} err the error to emit
2018-06-28 02:56:26 +00:00
* @param {number} fd the file descriptor
* @returns {void}
*/
2020-05-01 16:25:32 +00:00
const opened = (err: NodeJS.ErrnoException | null, fd: number) => {
2018-05-27 04:25:08 +00:00
if (!err) {
2020-05-01 16:25:32 +00:00
const read = (err: NodeJS.ErrnoException | null, bytes: number) => {
2018-05-27 04:25:08 +00:00
if (!err && this.readable) {
let encoding =
attachment && attachment.headers
? attachment.headers['content-transfer-encoding'] || 'base64'
: 'base64';
if (encoding === 'ascii' || encoding === '7bit') {
encoding = 'ascii';
} else if (encoding === 'binary' || encoding === '8bit') {
encoding = 'binary';
} else {
encoding = 'base64';
}
// guaranteed to be encoded without padding unless it is our last read
outputBase64(buffer.toString(encoding, 0, bytes), () => {
2018-05-27 04:25:08 +00:00
if (bytes == chunk) {
// we read a full chunk, there might be more
fs.read(fd, buffer, 0, chunk, null, read);
} // that was the last chunk, we are done reading the file
else {
this.removeListener('error', closed);
fs.close(fd, next);
}
});
} else {
this.emit(
'error',
err || { message: 'message stream was interrupted somehow!' }
);
}
};
2018-05-27 04:25:08 +00:00
fs.read(fd, buffer, 0, chunk, null, read);
this.once('error', closed);
} else {
this.emit('error', err);
}
};
2020-05-24 14:47:49 +00:00
fs.open(attachment.path as PathLike, 'r', opened);
};
2018-06-28 02:56:26 +00:00
/**
* @param {MessageAttachment} attachment the metadata to use as headers
2018-06-29 03:44:54 +00:00
* @param {function(): void} callback the function to call after output is finished
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
const outputStream = (
attachment: MessageAttachment,
2020-04-23 04:26:49 +00:00
callback: () => void
) => {
const { stream } = attachment;
if (stream?.readable) {
2018-06-28 02:57:21 +00:00
let previous = Buffer.alloc(0);
stream.resume();
stream.on('end', () => {
outputBase64(previous.toString('base64'), callback);
this.removeListener('pause', stream.pause);
this.removeListener('resume', stream.resume);
this.removeListener('error', stream.resume);
});
stream.on('data', (buff) => {
2018-05-27 04:25:08 +00:00
// do we have bytes from a previous stream data event?
2018-06-28 02:57:21 +00:00
let buffer = Buffer.isBuffer(buff) ? buff : Buffer.from(buff);
if (previous.byteLength > 0) {
2018-07-06 18:10:40 +00:00
buffer = Buffer.concat([previous, buffer]);
}
2018-05-27 04:25:08 +00:00
const padded = buffer.length % MIME64CHUNK;
2018-07-06 18:10:40 +00:00
previous = Buffer.alloc(padded);
2018-05-27 04:25:08 +00:00
// encode as much of the buffer to base64 without empty bytes
2018-06-28 02:57:21 +00:00
if (padded > 0) {
2018-05-27 04:25:08 +00:00
// copy dangling bytes into previous buffer
buffer.copy(previous, 0, buffer.length - padded);
}
outputBase64(buffer.toString('base64', 0, buffer.length - padded));
});
this.on('pause', stream.pause);
this.on('resume', stream.resume);
this.on('error', stream.resume);
2018-05-27 04:25:08 +00:00
} else {
this.emit('error', { message: 'stream not readable' });
}
};
const outputAttachment = (
attachment: MessageAttachment,
2020-05-24 14:47:49 +00:00
callback: () => void
) => {
const build = attachment.path
? outputFile
2020-05-24 14:47:49 +00:00
: attachment.stream
? outputStream
: outputData;
outputAttachmentHeaders(attachment);
2020-05-24 14:47:49 +00:00
build(attachment, callback);
};
2018-06-28 02:56:26 +00:00
/**
2020-05-24 14:47:49 +00:00
* @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
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
const outputMessage = (
2020-05-24 14:47:49 +00:00
boundary: string,
list: MessageAttachment[],
index: number,
callback: () => void
) => {
if (index < list.length) {
output(`--${boundary}${CRLF}`);
if (list[index].related) {
outputRelated(list[index], () =>
outputMessage(boundary, list, index + 1, callback)
2020-05-24 14:47:49 +00:00
);
} else {
outputAttachment(list[index], () =>
outputMessage(boundary, list, index + 1, callback)
2020-05-24 14:47:49 +00:00
);
}
} else {
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
2018-06-04 16:40:23 +00:00
callback();
}
};
const outputMixed = () => {
const boundary = generateBoundary();
2020-05-24 14:47:49 +00:00
output(
`Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
if (this.message.alternative == null) {
outputText(this.message);
outputMessage(boundary, this.message.attachments, 0, close);
2020-05-24 14:47:49 +00:00
} else {
outputAlternative(
2020-06-09 04:53:48 +00:00
// typescript bug; should narrow to { alternative: MessageAttachment }
this.message as Parameters<typeof outputAlternative>[0],
() => outputMessage(boundary, this.message.attachments, 0, close)
2020-05-24 14:47:49 +00:00
);
}
};
/**
* @param {MessageAttachment} attachment the metadata to use as headers
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const outputData = (
attachment: MessageAttachment,
2020-05-24 14:47:49 +00:00
callback: () => void
) => {
outputBase64(
2020-05-24 14:47:49 +00:00
attachment.encoded
? attachment.data ?? ''
: Buffer.from(attachment.data ?? '').toString('base64'),
2020-05-24 14:47:49 +00:00
callback
);
};
2018-06-28 02:56:26 +00:00
/**
* @param {Message} message the message to output
* @returns {void}
*/
const outputText = (message: Message) => {
2020-04-21 03:20:06 +00:00
let data: string[] = [];
2018-05-27 04:25:08 +00:00
data = data.concat([
'Content-Type:',
message.content,
CRLF,
'Content-Transfer-Encoding: 7bit',
CRLF,
]);
data = data.concat(['Content-Disposition: inline', CRLF, CRLF]);
data = data.concat([message.text || '', CRLF, CRLF]);
2018-05-27 04:25:08 +00:00
output(data.join(''));
};
2020-05-24 14:47:49 +00:00
/**
* @param {MessageAttachment} message the message to output
* @param {function(): void} callback the function to call after output is finished
* @returns {void}
*/
const outputRelated = (
message: MessageAttachment,
2020-05-24 14:47:49 +00:00
callback: () => void
) => {
const boundary = generateBoundary();
2020-05-24 14:47:49 +00:00
output(
`Content-Type: multipart/related; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
outputAttachment(message, () => {
outputMessage(boundary, message.related ?? [], 0, () => {
2020-05-24 14:47:49 +00:00
output(`${CRLF}--${boundary}--${CRLF}${CRLF}`);
callback();
});
});
};
2018-06-28 02:56:26 +00:00
/**
* @param {Message} message the message to output
2018-06-29 03:44:54 +00:00
* @param {function(): void} callback the function to call after output is finished
2018-06-28 02:56:26 +00:00
* @returns {void}
*/
const outputAlternative = (
message: Message & { alternative: MessageAttachment },
2020-04-23 04:26:49 +00:00
callback: () => void
) => {
const boundary = generateBoundary();
2018-05-27 04:25:08 +00:00
output(
`Content-Type: multipart/alternative; boundary="${boundary}"${CRLF}${CRLF}--${boundary}${CRLF}`
);
outputText(message);
output(`--${boundary}${CRLF}`);
2018-06-29 03:25:10 +00:00
/**
* @returns {void}
*/
const finish = () => {
2018-05-27 04:25:08 +00:00
output([CRLF, '--', boundary, '--', CRLF, CRLF].join(''));
callback();
};
2018-05-27 04:25:08 +00:00
if (message.alternative.related) {
outputRelated(message.alternative, finish);
2018-05-27 04:25:08 +00:00
} else {
outputAttachment(message.alternative, finish);
2018-05-27 04:25:08 +00:00
}
};
2020-05-24 14:47:49 +00:00
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');
};
2018-06-28 02:56:26 +00:00
/**
* @returns {void}
*/
const outputHeaderData = () => {
2018-05-27 04:25:08 +00:00
if (this.message.attachments.length || this.message.alternative) {
output(`MIME-Version: 1.0${CRLF}`);
outputMixed();
2018-05-27 04:25:08 +00:00
} // you only have a text message!
else {
outputText(this.message);
2018-05-27 04:25:08 +00:00
close();
}
};
2018-06-28 02:56:26 +00:00
/**
* @returns {void}
*/
const outputHeader = () => {
2020-04-21 03:20:06 +00:00
let data: string[] = [];
for (const header in this.message.header) {
2018-05-27 04:25:08 +00:00
// do not output BCC in the headers (regex) nor custom Object.prototype functions...
if (
!/bcc/i.test(header) &&
2020-05-24 14:47:49 +00:00
Object.prototype.hasOwnProperty.call(this.message.header, header)
2018-05-27 04:25:08 +00:00
) {
data = data.concat([
convertDashDelimitedTextToSnakeCase(header),
2018-05-27 04:25:08 +00:00
': ',
2020-05-24 14:47:49 +00:00
this.message.header[header] as string,
2018-05-27 04:25:08 +00:00
CRLF,
]);
}
}
2018-05-27 04:25:08 +00:00
output(data.join(''));
outputHeaderData();
};
2018-05-27 04:25:08 +00:00
this.once('destroy', close);
process.nextTick(outputHeader);
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2018-06-28 02:56:26 +00:00
* pause the stream
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public pause() {
2018-05-27 04:25:08 +00:00
this.paused = true;
this.emit('pause');
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2018-06-28 02:56:26 +00:00
* resume the stream
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public resume() {
2018-05-27 04:25:08 +00:00
this.paused = false;
this.emit('resume');
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2018-06-28 02:56:26 +00:00
* destroy the stream
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public destroy() {
2018-05-27 04:25:08 +00:00
this.emit(
'destroy',
this.bufferIndex > 0 ? { message: 'message stream destroyed' } : null
);
}
2018-06-28 02:56:26 +00:00
/**
2020-05-02 00:33:07 +00:00
* @public
2018-06-28 02:56:26 +00:00
* destroy the stream at first opportunity
* @returns {void}
*/
2020-05-02 00:33:07 +00:00
public destroySoon() {
2018-05-27 04:25:08 +00:00
this.emit('destroy');
}
}