smtp/connection: implement greylist support (#265)

* smtp/connection: implement greylist support

* smtp/connection: use weakmap to track if response has handled greylist

* smtp/connection: fix style nits

* test/client: fix greylist unit

* readme: note greylist support

* smtp/connection: accept code 451 for greylist resend

* smtp/connection: reorder greylist conditionals in response callback

* test/client: only respond once to greylist
This commit is contained in:
Zack Schuster 2020-07-25 18:44:33 -07:00 committed by GitHub
parent b1b6fad17b
commit b1a101d976
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 1 deletions

View File

@ -15,6 +15,7 @@ send emails, html and attachments (files, streams and strings) from node.js to a
- attachments can be added as strings, streams or file paths
- supports utf-8 headers and body
- built-in type declarations
- automatically handles [greylisting](http://projects.puremagic.com/greylisting/whitepaper.html)
## REQUIRES

View File

@ -35,6 +35,7 @@ const SMTP_PORT = 25 as const;
const SMTP_SSL_PORT = 465 as const;
const SMTP_TLS_PORT = 587 as const;
const CRLF = '\r\n' as const;
const GREYLIST_DELAY = 300 as const;
let DEBUG: 0 | 1 = 0;
@ -115,6 +116,11 @@ export class SMTPConnection extends EventEmitter {
protected tls: boolean | SMTPSocketOptions = false;
protected port: number;
private greylistResponseTracker = new WeakMap<
(...rest: any[]) => void,
boolean
>();
/**
* SMTP class written using python's (2.7) smtplib.py as a base.
*
@ -393,8 +399,18 @@ export class SMTPConnection extends EventEmitter {
if (err) {
caller(callback, err);
} else {
if (codesArray.indexOf(Number(msg.code)) !== -1) {
const code = Number(msg.code);
if (codesArray.indexOf(code) !== -1) {
caller(callback, err, msg.data, msg.message);
} else if (
(code === 450 || code === 451) &&
msg.message.toLowerCase().includes('greylist') &&
this.greylistResponseTracker.get(response) === false
) {
this.greylistResponseTracker.set(response, true);
setTimeout(() => {
this.send(cmd + CRLF, response);
}, GREYLIST_DELAY);
} else {
const suffix = msg.message ? `: ${msg.message}` : '';
const errorMessage = `bad response on command '${
@ -413,6 +429,7 @@ export class SMTPConnection extends EventEmitter {
}
};
this.greylistResponseTracker.set(response, false);
this.send(cmd + CRLF, response);
}

View File

@ -213,3 +213,80 @@ test('client constructor throws if `password` supplied without `user`', (t) => {
>)
);
});
test.cb('client supports greylisting', (t) => {
t.plan(2);
const msg = {
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
bcc: 'pooh@gmail.com',
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) => {
t.pass();
onRcptTo(a, s, cb);
};
const err = new Error('greylist');
((err as never) as { responseCode: number }).responseCode = 450;
callback(err);
};
client.send(new Message(msg), (err) => {
if (err) {
t.fail();
}
t.pass();
t.end();
});
});
test.cb('client only responds once to greylisting', (t) => {
t.plan(3);
const msg = {
subject: 'this is a test TEXT message from emailjs',
from: 'piglet@gmail.com',
bcc: 'pooh@gmail.com',
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) {
t.pass();
const err = new Error('greylist');
((err as never) as { responseCode: number }).responseCode = 450;
callback(err);
},
onAuth(auth, _session, callback) {
if (auth.username === 'pooh' && auth.password === 'honey') {
callback(null, { user: 'pooh' });
} else {
return callback(new Error('invalid user / pass'));
}
},
});
greylistServer.listen(greylistPort, () => {
greylistClient.send(new Message(msg), (err) => {
if (err) {
t.pass();
t.end();
} else {
t.fail();
}
});
});
});