mirror of https://github.com/eleith/emailjs.git
test: enable concurrency
This commit is contained in:
parent
0cc9d4f9e5
commit
341dd64972
|
@ -54,7 +54,7 @@
|
|||
"build": "rollup -c rollup.config.ts",
|
||||
"lint": "eslint *.ts \"+(smtp|test)/*.ts\"",
|
||||
"tsc": "tsc",
|
||||
"test": "ava --serial",
|
||||
"test": "ava",
|
||||
"test-cjs": "npm run build && npm run test -- --node-arguments='--title=cjs'"
|
||||
},
|
||||
"license": "MIT"
|
||||
|
|
333
test/auth.ts
333
test/auth.ts
|
@ -1,4 +1,4 @@
|
|||
import test from 'ava';
|
||||
import test, { CbExecutionContext } from 'ava';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import {
|
||||
SMTPServer,
|
||||
|
@ -32,327 +32,86 @@ function onAuth(
|
|||
}
|
||||
}
|
||||
|
||||
const port = 2526;
|
||||
let server: SMTPServer | null = null;
|
||||
|
||||
test.afterEach.cb((t) => server?.close(t.end));
|
||||
|
||||
test.cb('no authentication (unencrypted) should succeed', (t) => {
|
||||
let port = 1000;
|
||||
function send(
|
||||
t: CbExecutionContext,
|
||||
{
|
||||
authMethods = [],
|
||||
authOptional = false,
|
||||
secure = false,
|
||||
}: {
|
||||
authMethods?: (keyof typeof AUTH_METHODS)[];
|
||||
authOptional?: boolean;
|
||||
secure?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [],
|
||||
authOptional: true,
|
||||
const server = new SMTPServer({
|
||||
authMethods,
|
||||
secure: secure,
|
||||
hideSTARTTLS: !secure,
|
||||
authOptional,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
} as Record<string, unknown>).then((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);
|
||||
});
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({ port }).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
const p = port++;
|
||||
server.listen(p, () => {
|
||||
const options = Object.assign(
|
||||
{ port: p, ssl: secure, authentication: authMethods },
|
||||
authOptional ? {} : { user: 'pooh', password: 'honey' }
|
||||
);
|
||||
new SMTPClient(options).send(new Message(msg), (err) => {
|
||||
server.close();
|
||||
t.end(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.cb('no authentication (unencrypted) should succeed', (t) => {
|
||||
send(t, { authOptional: true });
|
||||
});
|
||||
|
||||
test.cb('no authentication (encrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [],
|
||||
authOptional: true,
|
||||
secure: true,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({ port, ssl: true }).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authOptional: true, secure: true });
|
||||
});
|
||||
|
||||
test.cb('PLAIN authentication (unencrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.PLAIN],
|
||||
hideSTARTTLS: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
authentication: [AUTH_METHODS.PLAIN],
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.PLAIN] });
|
||||
});
|
||||
|
||||
test.cb('PLAIN authentication (encrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.PLAIN],
|
||||
secure: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
authentication: [AUTH_METHODS.PLAIN],
|
||||
ssl: true,
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.PLAIN], secure: true });
|
||||
});
|
||||
|
||||
test.cb('LOGIN authentication (unencrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.LOGIN],
|
||||
hideSTARTTLS: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
authentication: [AUTH_METHODS.LOGIN],
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.LOGIN] });
|
||||
});
|
||||
|
||||
test.cb('LOGIN authentication (encrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.LOGIN],
|
||||
secure: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
authentication: [AUTH_METHODS.LOGIN],
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.LOGIN], secure: true });
|
||||
});
|
||||
|
||||
test.cb('XOAUTH2 authentication (unencrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.XOAUTH2],
|
||||
hideSTARTTLS: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
authentication: [AUTH_METHODS.XOAUTH2],
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.XOAUTH2] });
|
||||
});
|
||||
|
||||
test.cb('XOAUTH2 authentication (encrypted) should succeed', (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.",
|
||||
};
|
||||
server = new SMTPServer({
|
||||
authMethods: [AUTH_METHODS.XOAUTH2],
|
||||
secure: true,
|
||||
onAuth,
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then((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);
|
||||
})
|
||||
.finally(t.end);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
server.listen(port, () => {
|
||||
new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
authentication: [AUTH_METHODS.XOAUTH2],
|
||||
}).send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
send(t, { authMethods: [AUTH_METHODS.XOAUTH2], secure: true });
|
||||
});
|
||||
|
|
172
test/client.ts
172
test/client.ts
|
@ -1,4 +1,4 @@
|
|||
import test from 'ava';
|
||||
import test, { CbExecutionContext } from 'ava';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { SMTPServer } from 'smtp-server';
|
||||
|
||||
|
@ -6,51 +6,46 @@ import { DEFAULT_TIMEOUT, SMTPClient, Message } from '../email';
|
|||
|
||||
type UnPromisify<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
const port = 2526;
|
||||
const client = new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
});
|
||||
const server = new SMTPServer({ secure: true });
|
||||
let port = 2000;
|
||||
let greylistPort = 3000;
|
||||
|
||||
const send = (
|
||||
t: CbExecutionContext,
|
||||
message: Message,
|
||||
verify: (mail: UnPromisify<ReturnType<typeof simpleParser>>) => void,
|
||||
done: () => void
|
||||
verify: (mail: UnPromisify<ReturnType<typeof simpleParser>>) => void
|
||||
) => {
|
||||
server.onData = (stream, _session, callback: () => void) => {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then(verify)
|
||||
.finally(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) {
|
||||
const server = new SMTPServer({
|
||||
secure: true,
|
||||
onAuth(auth, _session, callback) {
|
||||
if (auth.username === 'pooh' && auth.password === 'honey') {
|
||||
callback(null, { user: 'pooh' });
|
||||
} else {
|
||||
return callback(new Error('invalid user / pass'));
|
||||
}
|
||||
};
|
||||
t.end();
|
||||
},
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>).then(verify);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.after.cb((t) => server.close(t.end));
|
||||
const p = port++;
|
||||
server.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
}).send(message, (err) => {
|
||||
server.close();
|
||||
t.end(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test.cb('client invokes callback exactly once for invalid connection', (t) => {
|
||||
t.plan(1);
|
||||
|
@ -62,7 +57,7 @@ test.cb('client invokes callback exactly once for invalid connection', (t) => {
|
|||
text: 'hello world',
|
||||
};
|
||||
client.send(new Message(msg), (err) => {
|
||||
t.not(err, null);
|
||||
t.true(err instanceof Error);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
@ -91,7 +86,7 @@ test('client deduplicates recipients', (t) => {
|
|||
cc: 'gannon@gmail.com',
|
||||
bcc: 'gannon@gmail.com',
|
||||
};
|
||||
const stack = client.createMessageStack(new Message(msg));
|
||||
const stack = new SMTPClient({}).createMessageStack(new Message(msg));
|
||||
t.true(stack.to.length === 1);
|
||||
t.is(stack.to[0].address, 'gannon@gmail.com');
|
||||
});
|
||||
|
@ -110,7 +105,7 @@ test.cb('client accepts array recipients', (t) => {
|
|||
|
||||
msg.valid((isValid) => {
|
||||
t.true(isValid);
|
||||
const stack = client.createMessageStack(msg);
|
||||
const stack = new SMTPClient({}).createMessageStack(msg);
|
||||
t.is(stack.to.length, 3);
|
||||
t.deepEqual(
|
||||
stack.to.map((x) => x.address),
|
||||
|
@ -139,7 +134,7 @@ test.cb('client rejects message without `from` header', (t) => {
|
|||
subject: 'this is a test TEXT message from emailjs',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
client.send(new Message(msg), (err) => {
|
||||
new SMTPClient({}).send(new Message(msg), (err) => {
|
||||
t.true(err instanceof Error);
|
||||
t.is(err?.message, 'Message must have a `from` header');
|
||||
t.end();
|
||||
|
@ -152,7 +147,7 @@ test.cb('client rejects message without `to`, `cc`, or `bcc` header', (t) => {
|
|||
from: 'piglet@gmail.com',
|
||||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
client.send(new Message(msg), (err) => {
|
||||
new SMTPClient({}).send(new Message(msg), (err) => {
|
||||
t.true(err instanceof Error);
|
||||
t.is(
|
||||
err?.message,
|
||||
|
@ -170,16 +165,12 @@ test.cb('client allows message with only `cc` recipient header', (t) => {
|
|||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
send(
|
||||
new 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.cc?.text, msg.cc);
|
||||
},
|
||||
t.end
|
||||
);
|
||||
send(t, new 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.cc?.text, msg.cc);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('client allows message with only `bcc` recipient header', (t) => {
|
||||
|
@ -190,16 +181,12 @@ test.cb('client allows message with only `bcc` recipient header', (t) => {
|
|||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
send(
|
||||
new 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.bcc, undefined);
|
||||
},
|
||||
t.end
|
||||
);
|
||||
send(t, new 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.bcc, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test('client constructor throws if `password` supplied without `user`', (t) => {
|
||||
|
@ -215,7 +202,7 @@ test('client constructor throws if `password` supplied without `user`', (t) => {
|
|||
});
|
||||
|
||||
test.cb('client supports greylisting', (t) => {
|
||||
t.plan(2);
|
||||
t.plan(3);
|
||||
|
||||
const msg = {
|
||||
subject: 'this is a test TEXT message from emailjs',
|
||||
|
@ -224,10 +211,28 @@ test.cb('client supports greylisting', (t) => {
|
|||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
const { onRcptTo } = server;
|
||||
server.onRcptTo = (_address, _session, callback) => {
|
||||
server.onRcptTo = (a, s, cb) => {
|
||||
const greylistServer = new SMTPServer({
|
||||
secure: true,
|
||||
onRcptTo(_address, _session, callback) {
|
||||
t.pass();
|
||||
callback();
|
||||
},
|
||||
onAuth(auth, _session, callback) {
|
||||
if (auth.username === 'pooh' && auth.password === 'honey') {
|
||||
callback(null, { user: 'pooh' });
|
||||
} else {
|
||||
return callback(new Error('invalid user / pass'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { onRcptTo } = greylistServer;
|
||||
greylistServer.onRcptTo = (_address, _session, callback) => {
|
||||
greylistServer.onRcptTo = (a, s, cb) => {
|
||||
t.pass();
|
||||
const err = new Error('greylist');
|
||||
((err as never) as { responseCode: number }).responseCode = 450;
|
||||
greylistServer.onRcptTo = onRcptTo;
|
||||
onRcptTo(a, s, cb);
|
||||
};
|
||||
|
||||
|
@ -236,12 +241,21 @@ test.cb('client supports greylisting', (t) => {
|
|||
callback(err);
|
||||
};
|
||||
|
||||
client.send(new Message(msg), (err) => {
|
||||
if (err) {
|
||||
t.fail();
|
||||
}
|
||||
t.pass();
|
||||
t.end();
|
||||
const p = greylistPort++;
|
||||
greylistServer.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
}).send(new Message(msg), (err) => {
|
||||
greylistServer.close();
|
||||
if (err) {
|
||||
t.fail();
|
||||
}
|
||||
t.pass();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -255,13 +269,6 @@ test.cb('client only responds once to greylisting', (t) => {
|
|||
text: "It is hard to be brave when you're only a Very Small Animal.",
|
||||
};
|
||||
|
||||
const greylistPort = 2527;
|
||||
const greylistClient = new SMTPClient({
|
||||
port: greylistPort,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
});
|
||||
const greylistServer = new SMTPServer({
|
||||
secure: true,
|
||||
onRcptTo(_address, _session, callback) {
|
||||
|
@ -279,8 +286,15 @@ test.cb('client only responds once to greylisting', (t) => {
|
|||
},
|
||||
});
|
||||
|
||||
greylistServer.listen(greylistPort, () => {
|
||||
greylistClient.send(new Message(msg), (err) => {
|
||||
const p = greylistPort++;
|
||||
greylistServer.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
}).send(new Message(msg), (err) => {
|
||||
greylistServer.close();
|
||||
if (err) {
|
||||
t.pass();
|
||||
t.end();
|
||||
|
|
354
test/message.ts
354
test/message.ts
|
@ -1,58 +1,52 @@
|
|||
import { readFileSync, createReadStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import test from 'ava';
|
||||
import test, { CbExecutionContext } from 'ava';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { SMTPServer } from 'smtp-server';
|
||||
|
||||
import { SMTPClient, Message, MessageAttachment } from '../email';
|
||||
|
||||
const port = 2526;
|
||||
const client = new SMTPClient({
|
||||
port,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
});
|
||||
const server = new SMTPServer({ secure: true });
|
||||
|
||||
type UnPromisify<T> = T extends Promise<infer U> ? U : T;
|
||||
const send = (
|
||||
message: Message,
|
||||
verify: (mail: UnPromisify<ReturnType<typeof simpleParser>>) => void,
|
||||
done: () => void
|
||||
) => {
|
||||
server.onData = (stream, _session, callback: () => void) => {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>)
|
||||
.then(verify)
|
||||
.finally(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) {
|
||||
let port = 4000;
|
||||
|
||||
function send(
|
||||
t: CbExecutionContext,
|
||||
message: Message,
|
||||
verify: (mail: UnPromisify<ReturnType<typeof simpleParser>>) => void
|
||||
) {
|
||||
const server = new SMTPServer({
|
||||
secure: true,
|
||||
onAuth(auth, _session, callback) {
|
||||
if (auth.username == 'pooh' && auth.password == 'honey') {
|
||||
callback(null, { user: 'pooh' });
|
||||
} else {
|
||||
return callback(new Error('invalid user / pass'));
|
||||
}
|
||||
};
|
||||
t.end();
|
||||
},
|
||||
onData(stream, _session, callback: () => void) {
|
||||
simpleParser(stream, {
|
||||
skipHtmlToText: true,
|
||||
skipTextToHtml: true,
|
||||
skipImageLinks: true,
|
||||
} as Record<string, unknown>).then(verify);
|
||||
stream.on('end', callback);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.after.cb((t) => server.close(t.end));
|
||||
const p = port++;
|
||||
server.listen(p, () => {
|
||||
new SMTPClient({
|
||||
port: p,
|
||||
user: 'pooh',
|
||||
password: 'honey',
|
||||
ssl: true,
|
||||
}).send(message, (err) => {
|
||||
server.close();
|
||||
t.end(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.cb('simple text message', (t) => {
|
||||
const msg = {
|
||||
|
@ -65,17 +59,13 @@ test.cb('simple text message', (t) => {
|
|||
'message-id': 'this is a special id',
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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'] + '>');
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('null text message', (t) => {
|
||||
|
@ -87,13 +77,9 @@ test.cb('null text message', (t) => {
|
|||
'message-id': 'this is a special id',
|
||||
};
|
||||
|
||||
send(
|
||||
new Message(msg),
|
||||
(mail) => {
|
||||
t.is(mail.text, '\n\n\n');
|
||||
},
|
||||
t.end
|
||||
);
|
||||
send(t, new Message(msg), (mail) => {
|
||||
t.is(mail.text, '\n\n\n');
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('empty text message', (t) => {
|
||||
|
@ -105,13 +91,9 @@ test.cb('empty text message', (t) => {
|
|||
'message-id': 'this is a special id',
|
||||
};
|
||||
|
||||
send(
|
||||
new Message(msg),
|
||||
(mail) => {
|
||||
t.is(mail.text, '\n\n\n');
|
||||
},
|
||||
t.end
|
||||
);
|
||||
send(t, new Message(msg), (mail) => {
|
||||
t.is(mail.text, '\n\n\n');
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('simple unicode text message', (t) => {
|
||||
|
@ -122,16 +104,12 @@ test.cb('simple unicode text message', (t) => {
|
|||
text: 'hello ✓ friend, i hope this message finds you well.',
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('very large text message', (t) => {
|
||||
|
@ -143,16 +121,12 @@ test.cb('very large text message', (t) => {
|
|||
text: readFileSync(join(__dirname, 'attachments/smtp.txt'), 'utf-8'),
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('very large text data message', (t) => {
|
||||
|
@ -173,17 +147,13 @@ test.cb('very large text data message', (t) => {
|
|||
},
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('html data message', (t) => {
|
||||
|
@ -198,17 +168,13 @@ test.cb('html data message', (t) => {
|
|||
},
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('html file message', (t) => {
|
||||
|
@ -223,17 +189,13 @@ test.cb('html file message', (t) => {
|
|||
},
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('html with image embed message', (t) => {
|
||||
|
@ -257,21 +219,17 @@ test.cb('html with image embed message', (t) => {
|
|||
},
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('html data and attachment message', (t) => {
|
||||
|
@ -286,17 +244,13 @@ test.cb('html data and attachment message', (t) => {
|
|||
] as MessageAttachment[],
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('attachment message', (t) => {
|
||||
|
@ -313,20 +267,16 @@ test.cb('attachment message', (t) => {
|
|||
} as MessageAttachment,
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('attachment sent with unicode filename message', (t) => {
|
||||
|
@ -343,21 +293,17 @@ test.cb('attachment sent with unicode filename message', (t) => {
|
|||
} as MessageAttachment,
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('attachments message', (t) => {
|
||||
|
@ -382,24 +328,20 @@ test.cb('attachments message', (t) => {
|
|||
] as MessageAttachment[],
|
||||
};
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('streams message', (t) => {
|
||||
|
@ -429,24 +371,20 @@ test.cb('streams message', (t) => {
|
|||
stream.pause();
|
||||
stream2.pause();
|
||||
|
||||
send(
|
||||
new 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
|
||||
);
|
||||
send(t, new 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);
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('message validation fails without `from` header', (t) => {
|
||||
|
|
14
test/mime.ts
14
test/mime.ts
|
@ -2,23 +2,23 @@
|
|||
import test from 'ava';
|
||||
import { mimeEncode, mimeWordEncode } from '../email';
|
||||
|
||||
test('mimeEncode should encode UTF-8', (t) => {
|
||||
test('mimeEncode should encode UTF-8', async (t) => {
|
||||
t.is(mimeEncode('tere ÕÄÖÕ'), 'tere =C3=95=C3=84=C3=96=C3=95');
|
||||
});
|
||||
|
||||
test('mimeEncode should encode trailing whitespace', (t) => {
|
||||
test('mimeEncode should encode trailing whitespace', async (t) => {
|
||||
t.is(mimeEncode('tere '), 'tere =20');
|
||||
});
|
||||
|
||||
test('mimeEncode should encode non UTF-8', (t) => {
|
||||
test('mimeEncode should encode non UTF-8', async (t) => {
|
||||
t.is(mimeEncode(new Uint8Array([0xbd, 0xc5]), 'utf-16be'), '=EB=B7=85');
|
||||
});
|
||||
|
||||
test('mimeWordEncode should encode', (t) => {
|
||||
test('mimeWordEncode should encode', async (t) => {
|
||||
t.is('=?UTF-8?Q?See_on_=C3=B5hin_test?=', mimeWordEncode('See on õhin test'));
|
||||
});
|
||||
|
||||
test('mimeWordEncode should QP-encode mime word', (t) => {
|
||||
test('mimeWordEncode should QP-encode mime word', async (t) => {
|
||||
t.is(
|
||||
'=?UTF-8?Q?=E4=AB=B5=E6=9D=A5=E2=B5=B6=E6=87=9E?=',
|
||||
mimeWordEncode(
|
||||
|
@ -29,14 +29,14 @@ test('mimeWordEncode should QP-encode mime word', (t) => {
|
|||
);
|
||||
});
|
||||
|
||||
test('mimeWordEncode should Base64-encode mime word', (t) => {
|
||||
test('mimeWordEncode should Base64-encode mime word', async (t) => {
|
||||
t.is(
|
||||
mimeWordEncode('Привет и до свидания', 'B'),
|
||||
'=?UTF-8?B?0J/RgNC40LLQtdGCINC4INC00L4g0YHQstC40LTQsNC90LjRjw==?='
|
||||
);
|
||||
});
|
||||
|
||||
test('mimeWordEncode should Base64-encode a long mime word', (t) => {
|
||||
test('mimeWordEncode should Base64-encode a long mime word', async (t) => {
|
||||
const payload =
|
||||
'üöß‹€Привет и до свиданияПривет и до свиданияПривет и до свиданияПривет и до свиданияПривет и до свиданияПривет и до свиданияПривет и до свиданияПривет и до свидания';
|
||||
const expected =
|
||||
|
|
Loading…
Reference in New Issue