UNPKG

smtp-server

Version:

Create custom SMTP servers on the fly

1,459 lines (1,247 loc) 83.1 kB
/* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */ 'use strict'; const chai = require('chai'); const Client = require('nodemailer/lib/smtp-connection'); const XOAuth2 = require('nodemailer/lib/xoauth2'); const SMTPServer = require('../lib/smtp-server').SMTPServer; const SMTPConnection = require('../lib/smtp-connection').SMTPConnection; const net = require('net'); const pem = require('pem'); const expect = chai.expect; const fs = require('fs'); chai.config.includeStack = true; describe('SMTPServer', function () { this.timeout(10 * 1000); // eslint-disable-line no-invalid-this describe('Unit tests', function () { describe('Session ID generation', function () { it('should generate valid session IDs with correct format', function () { let conn = new SMTPConnection( { options: {} }, {} ); // ID should be 16 characters long expect(conn.id).to.have.length(16); // ID should only contain base32 characters (0-9, a-v) expect(conn.id).to.match(/^[0-9a-v]+$/); }); it('should generate unique session IDs', function () { let ids = new Set(); for (let i = 0; i < 100; i++) { let conn = new SMTPConnection( { options: {} }, {} ); ids.add(conn.id); } // All 100 IDs should be unique expect(ids.size).to.equal(100); }); it('should use provided ID from options', function () { let customId = 'mycustomsessionid'; let conn = new SMTPConnection( { options: {} }, {}, { id: customId } ); expect(conn.id).to.equal(customId); }); it('should set session.id to match connection id', function () { let conn = new SMTPConnection( { options: {} }, {} ); expect(conn.session.id).to.equal(conn.id); }); it('should handle edge case of small random values (leading zeros)', function () { // Test that IDs are always 16 chars even with small random values // by verifying padding works correctly for (let i = 0; i < 50; i++) { let conn = new SMTPConnection( { options: {} }, {} ); expect(conn.id).to.have.length(16); // Verify no undefined or NaN characters expect(conn.id).to.not.include('undefined'); expect(conn.id).to.not.include('NaN'); } }); }); describe('#_parseAddressCommand', function () { it('should parse MAIL FROM/RCPT TO', function () { let conn = new SMTPConnection( { options: {} }, {} ); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com>')).to.deep.equal({ address: 'test@example.com', args: false }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@example.com> SIZE=12345 RET=HDRS ')).to.deep.equal({ address: 'sender@example.com', args: { SIZE: '12345', RET: 'HDRS' } }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM : <test@example.com>')).to.deep.equal({ address: 'test@example.com', args: false }); expect(conn._parseAddressCommand('MAIL TO', 'MAIL FROM:<test@example.com>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@example.com> CUSTOM=a+ABc+20foo')).to.deep.equal({ address: 'sender@example.com', args: { CUSTOM: 'a\xabc foo' } }); }); it('should parse IPv4 literal addresses', function () { let conn = new SMTPConnection( { options: {} }, {} ); expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[192.168.5.146]>')).to.deep.equal({ address: 'test@[192.168.5.146]', args: false }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@[10.0.0.1]>')).to.deep.equal({ address: 'sender@[10.0.0.1]', args: false }); // With additional parameters expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@[127.0.0.1]> SIZE=1234')).to.deep.equal({ address: 'sender@[127.0.0.1]', args: { SIZE: '1234' } }); }); it('should parse IPv6 literal addresses', function () { let conn = new SMTPConnection( { options: {} }, {} ); expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[IPv6:2001:db8::1]>')).to.deep.equal({ address: 'test@[IPv6:2001:db8::1]', args: false }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@[IPv6:::1]>')).to.deep.equal({ address: 'sender@[IPv6:::1]', args: false }); // Case insensitive IPv6 prefix expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[ipv6:fe80::1]>')).to.deep.equal({ address: 'test@[IPv6:fe80::1]', args: false }); }); it('should reject invalid IP literal addresses', function () { let conn = new SMTPConnection( { options: {} }, {} ); // Invalid IPv4 (out of range) expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[999.999.999.999]>')).to.be.false; // Invalid IPv4 format expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[192.168.1]>')).to.be.false; // Invalid IPv6 expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[IPv6:invalid]>')).to.be.false; // Empty brackets expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[]>')).to.be.false; // Random content in brackets (not a valid IP) expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[notanip]>')).to.be.false; }); it('should normalize full form IPv6 addresses', function () { let conn = new SMTPConnection( { options: {} }, {} ); // Full form IPv6 should be normalized to compressed form expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[IPv6:2001:0db8:0000:0000:0000:0000:0000:0001]>')).to.deep.equal({ address: 'test@[IPv6:2001:db8::1]', args: false }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@[IPv6:0000:0000:0000:0000:0000:0000:0000:0001]>')).to.deep.equal({ address: 'sender@[IPv6:::1]', args: false }); }); it('should accept IPv4-mapped IPv6 addresses', function () { let conn = new SMTPConnection( { options: {} }, {} ); // IPv4-mapped IPv6 addresses are valid per RFC 4291 expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[IPv6:::ffff:192.168.1.1]>')).to.deep.equal({ address: 'test@[IPv6:::ffff:192.168.1.1]', args: false }); // IPv4-mapped in hex form expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@[IPv6:::ffff:c0a8:0101]>')).to.deep.equal({ address: 'test@[IPv6:::ffff:c0a8:101]', args: false }); }); it('should reject malformed addresses by default', function () { let conn = new SMTPConnection( { options: {} }, {} ); // local part with trailing dot (eg. HPE iLO firmware) expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<ilo.@example.com>')).to.be.false; // leading dot and consecutive dots in local part expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<.test@example.com>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<te..st@example.com>')).to.be.false; // malformed domain expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example..com>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com.>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@.example.com>')).to.be.false; }); it('should accept malformed addresses in lenient mode', function () { let conn = new SMTPConnection( { options: { lenientAddressParsing: true } }, {} ); // local part with trailing dot (eg. HPE iLO firmware) expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<ilo.@example.com>')).to.deep.equal({ address: 'ilo.@example.com', args: false }); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<.te..st.@example..com.> SIZE=12345')).to.deep.equal({ address: '.te..st.@example..com.', args: { SIZE: '12345' } }); // length limits are not enforced let longLocal = 'a'.repeat(300); expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<' + longLocal + '@example.com>')).to.deep.equal({ address: longLocal + '@example.com', args: false }); // basic structure is still required expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<@example.com>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:test@example.com')).to.be.false; // empty address (null return path) is still allowed expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<>')).to.deep.equal({ address: '', args: false }); }); it('should reject control and invisible characters in addresses even in lenient mode', function () { let strictConn = new SMTPConnection( { options: {} }, {} ); let lenientConn = new SMTPConnection( { options: { lenientAddressParsing: true } }, {} ); for (let conn of [strictConn, lenientConn]) { // NUL byte in local part expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<te\x00st@example.com>')).to.be.false; // escape and DEL characters in local part expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<te\x1bst@example.com>')).to.be.false; expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<te\x7fst@example.com>')).to.be.false; // C1 control character in local part expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<te\u0085st@example.com>')).to.be.false; // zero-width space and word joiner in domain expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@exam\u200Bple.com>')).to.be.false; expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@exam\u2060ple.com>')).to.be.false; // zero-width no-break space in domain expect(conn._parseAddressCommand('RCPT TO', 'RCPT TO:<test@exam\uFEFFple.com>')).to.be.false; } }); it('should reject parameter values with control characters', function () { let conn = new SMTPConnection( { options: {} }, {} ); // raw NUL byte in parameter value expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com> ENVID=ab\x00cd')).to.be.false; // CRLF smuggled through xtext encoding expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com> ENVID=ab+0D+0Acd')).to.be.false; // properly encoded printable characters are still decoded expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com> ENVID=ab+2Bcd')).to.deep.equal({ address: 'test@example.com', args: { ENVID: 'ab+cd' } }); }); }); }); describe('Plaintext server', function () { let PORT; let server; beforeEach(function (done) { server = new SMTPServer({ maxClients: 5, logger: false, socketTimeout: 2 * 1000 }); server.listen(0, '127.0.0.1', err => { if (err) return done(err); PORT = server.server.address().port; done(); }); }); afterEach(function (done) { server.close(done); }); it('should connect without TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', ignoreTLS: true }); connection.on('end', done); connection.connect(function () { connection.quit(); }); }); it('should connect with TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.quit(); }); }); it('open multiple connections', function (done) { let limit = 5; let disconnected = 0; let connected = 0; let connections = []; let createConnection = function (callback) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('error', function (err) { connected++; expect(err).to.not.exist; connection.close(); }); connection.on('end', function () { disconnected++; if (disconnected >= limit) { return done(); } }); connection.connect(function () { connected++; callback(null, connection); }); }; let connCb = function (err, conn) { expect(err).to.not.exist; connections.push(conn); if (connected >= limit) { connections.forEach(function (connection) { connection.close(); }); } }; for (let i = 0; i < limit; i++) { createConnection(connCb); } }); it('should reject too many connections', function (done) { let limit = 7; let expectedErrors = 2; let disconnected = 0; let connected = 0; let connections = []; let createConnection = function (callback) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('error', function (err) { connected++; if (!expectedErrors) { expect(err).to.not.exist; } else { expectedErrors--; } connection.close(); }); connection.on('end', function () { disconnected++; if (disconnected >= limit) { return done(); } }); connection.connect(function () { connected++; callback(null, connection); }); }; let connCb = function (err, conn) { expect(err).to.not.exist; connections.push(conn); if (connected >= limit) { connections.forEach(function (connection) { connection.close(); }); } }; for (let i = 0; i < limit; i++) { createConnection(connCb); } }); it('should close on timeout', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', ignoreTLS: true }); connection.on('error', function (err) { expect(err).to.exist; }); connection.on('end', done); connection.connect(function () { // do nothing, wait until timeout occurs }); }); it('should close on timeout using secure socket', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('error', function (err) { expect(err).to.exist; }); connection.on('end', done); connection.connect(function () { // do nothing, wait until timeout occurs }); }); }); describe('Plaintext server with no connection limit', function () { this.timeout(60 * 1000); // eslint-disable-line no-invalid-this let PORT = 1336; let server = new SMTPServer({ logger: false, socketTimeout: 100 * 1000, closeTimeout: 6 * 1000 }); beforeEach(function (done) { server.listen(PORT, '127.0.0.1', done); }); it('open multiple connections and close all at once', function (done) { let limit = 100; let cleanClose = 4; let disconnected = 0; let connected = 0; let connections = []; let createConnection = function (callback) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('error', function (err) { expect(err.responseCode).to.equal(421); // Server shutting down }); connection.on('end', function () { disconnected++; if (disconnected >= limit) { return done(); } }); connection.connect(function () { connected++; callback(null, connection); }); }; let connCb = function (err, conn) { expect(err).to.not.exist; connections.push(conn); if (connected >= limit) { server.close(); setTimeout(function () { for (let i = 0; i < cleanClose; i++) { connections[i].quit(); } }, 1000); } else { createConnection(connCb); } }; createConnection(connCb); }); }); describe('Plaintext server with hidden STARTTLS', function () { let PORT; let server; beforeEach(function (done) { server = new SMTPServer({ maxClients: 5, hideSTARTTLS: true, logger: false, socketTimeout: 2 * 1000 }); server.listen(0, '127.0.0.1', err => { if (err) return done(err); PORT = server.server.address().port; done(); }); }); afterEach(function (done) { server.close(done); }); it('should connect without TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1' }); connection.on('end', done); connection.connect(function () { expect(connection.secure).to.be.false; connection.quit(); }); }); it('should connect with TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', requireTLS: true, tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { expect(connection.secure).to.be.true; connection.quit(); }); }); }); describe('Plaintext server with no STARTTLS', function () { let PORT; let server; beforeEach(function (done) { server = new SMTPServer({ maxClients: 5, disabledCommands: ['STARTTLS'], logger: false, socketTimeout: 2 * 1000, onAuth(auth, session, callback) { expect(session.tlsOptions).to.be.false; if (auth.username === 'testuser' && auth.password === 'testpass') { return callback(null, { user: 'userdata' }); } else { return callback(null, { message: 'Authentication failed' }); } } }); server.listen(0, '127.0.0.1', err => { if (err) return done(err); PORT = server.server.address().port; done(); }); }); afterEach(function (done) { server.close(done); }); it('should connect without TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1' }); connection.on('end', done); connection.connect(function () { expect(connection.secure).to.be.false; connection.quit(); }); }); it('should not connect with TLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', requireTLS: true, tls: { rejectUnauthorized: false } }); let error; connection.on('error', function (err) { error = err; }); connection.on('end', function () { expect(error).to.exist; done(); }); connection.connect(function () { // should not be called expect(false).to.be.true; connection.quit(); }); }); it('should close after too many unauthenticated commands', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', ignoreTLS: true }); connection.on('error', function (err) { expect(err).to.exist; }); connection.on('end', done); connection.connect(function () { let looper = function () { connection._currentAction = function () { looper(); }; connection._sendCommand('NOOP'); }; looper(); }); }); it('should close after too many unrecognized commands', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', ignoreTLS: true }); connection.on('error', function (err) { expect(err).to.exist; }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'testuser', pass: 'testpass' }, function (err) { expect(err).to.not.exist; let looper = function () { connection._currentAction = function () { looper(); }; connection._sendCommand('ZOOP'); }; looper(); } ); }); }); it('should close connection when command line is too long', function (done) { let socket = net.connect(PORT, '127.0.0.1', function () { let buffers = []; let started = false; socket.on('data', function (chunk) { buffers.push(chunk); if (!started) { started = true; // send a large chunk with no newline to trigger maxCommandLength let longData = Buffer.alloc(5 * 1024, 0x41); socket.write(longData); } }); socket.on('end', function () { let data = Buffer.concat(buffers).toString(); expect(data).to.include('421'); expect(data).to.include('Command line too long'); done(); }); }); }); it('should reject early talker', function (done) { let socket = net.connect(PORT, '127.0.0.1', function () { let buffers = []; socket.on('data', function (chunk) { buffers.push(chunk); }); socket.on('end', function () { let data = Buffer.concat(buffers).toString(); expect(/^421 /.test(data)).to.be.true; done(); }); socket.write('EHLO FOO\r\n'); }); }); it('should reject HTTP requests', function (done) { let socket = net.connect(PORT, '127.0.0.1', function () { let buffers = []; let started = false; socket.on('data', function (chunk) { buffers.push(chunk); if (!started) { started = true; socket.write('GET /path/file.html HTTP/1.0\r\nHost: www.example.com\r\n\r\n'); } }); socket.on('end', function () { let data = Buffer.concat(buffers).toString(); expect(/^421 /m.test(data)).to.be.true; done(); }); }); }); }); describe('Secure server', function () { let PORT = 1336; let server = new SMTPServer({ secure: true, logger: false }); beforeEach(function (done) { server.listen(PORT, '127.0.0.1', done); }); afterEach(function (done) { server.close(function () { done(); }); }); it('should connect to secure server', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', secure: true, tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.quit(); }); }); }); describe('Secure server with upgrade', function () { let PORT = 1336; let server = new SMTPServer({ secure: true, needsUpgrade: true, logger: false }); beforeEach(function (done) { server.listen(PORT, '127.0.0.1', done); }); afterEach(function (done) { server.close(function () { done(); }); }); it('should connect to secure server', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', secure: true, tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.quit(); }); }); }); describe('Secure server with cert update', function () { let PORT = 1336; let server; beforeEach(function (done) { pem.createCertificate({ days: 1, selfSigned: true }, (err, keys) => { if (err) { return done(err); } server = new SMTPServer({ secure: true, logger: false, key: keys.serviceKey, cert: keys.certificate }); server.listen(PORT, '127.0.0.1', done); }); }); afterEach(function (done) { server.close(function () { done(); }); }); it('should connect to secure server', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', secure: true, tls: { rejectUnauthorized: false } }); let firstFingerprint; connection.connect(() => { firstFingerprint = connection._socket.getPeerCertificate().fingerprint; connection.quit(); }); connection.on('end', () => { pem.createCertificate({ days: 1, selfSigned: true }, (err, keys) => { if (err) { return done(err); } server.updateSecureContext({ key: keys.serviceKey, cert: keys.certificate }); setTimeout(() => { let connection = new Client({ port: PORT, host: '127.0.0.1', secure: true, tls: { rejectUnauthorized: false } }); connection.connect(() => { let secondFingerprint = connection._socket.getPeerCertificate().fingerprint; expect(firstFingerprint).to.not.equal(secondFingerprint); connection.quit(); }); connection.on('end', done); }, 1000); }); }); }); }); describe('Authentication tests', function () { let PORT; let server; beforeEach(function (done) { server = new SMTPServer({ maxClients: 5, logger: false, authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2', 'CRAM-MD5'], allowInsecureAuth: true, onAuth(auth, session, callback) { expect(session.tlsOptions).to.exist; if (auth.method === 'XOAUTH2') { if (auth.username === 'testuser' && auth.accessToken === 'testtoken') { return callback(null, { user: 'userdata' }); } else { return callback(null, { data: { status: '401', schemes: 'bearer mac', scope: 'https://mail.google.com/' } }); } } else if ( auth.username === 'testuser' && (auth.method === 'CRAM-MD5' ? auth.validatePassword('testpass') : auth.password === 'testpass') ) { return callback(null, { user: 'userdata' }); } else { return callback(null, { message: 'Authentication failed' }); } } }); server.listen(0, '127.0.0.1', err => { if (err) return done(err); PORT = server.server.address().port; done(); }); }); afterEach(function (done) { server.close(done); }); describe('PLAIN', function () { it('should authenticate', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'testuser', pass: 'testpass', method: 'PLAIN' }, function (err) { expect(err).to.not.exist; connection.quit(); } ); }); }); it('should fail', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'zzzz', pass: 'yyyy', method: 'PLAIN' }, function (err) { expect(err).to.exist; connection.quit(); } ); }); }); }); describe('LOGIN', function () { it('should authenticate', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false }, logger: false }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'testuser', pass: 'testpass', method: 'LOGIN' }, function (err) { expect(err).to.not.exist; connection.quit(); } ); }); }); it('should authenticate without STARTTLS', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', ignoreTLS: true, logger: false }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'testuser', pass: 'testpass', method: 'LOGIN' }, function (err) { expect(err).to.not.exist; connection.quit(); } ); }); }); it('should fail', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'zzzz', pass: 'yyyy', method: 'LOGIN' }, function (err) { expect(err).to.exist; connection.quit(); } ); }); }); }); describe('XOAUTH2', function () { it('should authenticate', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { type: 'oauth2', user: 'testuser', method: 'XOAUTH2', oauth2: new XOAuth2( { user: 'testuser', accessToken: 'testtoken' }, false ) }, function (err) { expect(err).to.not.exist; connection.quit(); } ); }); }); it('should fail', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { type: 'oauth2', user: 'zzzz', method: 'XOAUTH2', oauth2: new XOAuth2( { user: 'zzzz', accessToken: 'testtoken' }, false ) }, function (err) { expect(err).to.exist; connection.quit(); } ); }); }); }); describe('CRAM-MD5', function () { it('should authenticate', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'testuser', pass: 'testpass', method: 'CRAM-MD5' }, function (err) { expect(err).to.not.exist; connection.quit(); } ); }); }); it('should fail', function (done) { let connection = new Client({ port: PORT, host: '127.0.0.1', tls: { rejectUnauthorized: false } }); connection.on('end', done); connection.connect(function () { connection.login( { user: 'zzzz', pass: 'yyyy', method: 'CRAM-MD5' }, function (err) { expect(err).to.exist; connection.quit(); } ); }); }); }); }); describe('SASL identity handling', function () { let PORT; let server; let lastAuth; beforeEach(function (done) { lastAuth = false; server = new SMTPServer({ maxClients: 5, logger: false, authMethods: ['PLAIN', 'LOGIN'], allowInsecureAuth: true, onAuth(auth, session, callback) { lastAuth = auth; if (auth.username === 'testuser' && auth.password === 'testpass') { return callback(null, { user: 'userdata' }); } return callback(null, { message: 'Authentication failed' }); } }); server.listen(0, '127.0.0.1', err => { if (err) return done(err); PORT = server.server.address().port; done(); }); }); afterEach(function (done) { server.close(done); }); // connects, sends EHLO and then one command after every final response, // ending with QUIT. Calls onClose with all collected response data function exchange(commands, onClose) { commands = ['EHLO example.com'].concat(commands).concat('QUIT'); let socket = net.connect(PORT, '127.0.0.1', function () { let buffers = []; let sent = 0; socket.on('data', function (chunk) { buffers.push(chunk); let data = Buffer.concat(buffers).toString(); // count final response lines (status code not followed by a dash) let finals = (data.match(/^\d{3}(?!-)/gm) || []).length; while (sent < commands.length && finals > sent) { socket.write(commands[sent++] + '\r\n'); } }); socket.on('end', function () { onClose(Buffer.concat(buffers).toString()); }); }); } it('should fall back to authzid when authcid is empty', function (done) { // some clients place the username in the authzid field, eg. testuser\x00\x00testpass let token = Buffer.from('testuser\x00\x00testpass').toString('base64'); exchange(['AUTH PLAIN ' + token], function (data) { expect(data).to.include('235 Authentication successful'); expect(lastAuth.method).to.equal('PLAIN'); expect(lastAuth.username).to.equal('testuser'); expect(lastAuth.authcid).to.equal(''); expect(lastAuth.authzid).to.equal('testuser'); done(); }); }); it('should expose authzid and authcid for PLAIN', function (done) { let token = Buffer.from('admin\x00testuser\x00testpass').toString('base64'); exchange(['AUTH PLAIN ' + token], function (data) { expect(data).to.include('235 Authentication successful'); expect(lastAuth.method).to.equal('PLAIN'); expect(lastAuth.username).to.equal('testuser'); expect(lastAuth.authcid).to.equal('testuser'); expect(lastAuth.authzid).to.equal('admin'); done(); }); }); it('should reject PLAIN token with control characters in username', function (done) { // CRLF injection attempt through the authzid field let token = Buffer.from('attacker\r\nReceived: evil\x00\x00p').toString('base64'); exchange(['AUTH PLAIN ' + token], function (data) { expect(data).to.include('500 Error: invalid userdata'); // onAuth must never see the poisoned username expect(lastAuth).to.equal(false); done(); }); }); it('should reject LOGIN username with control characters', function (done) { let username = Buffer.from('attacker\r\nReceived: evil').toString('base64'); exchange(['AUTH LOGIN', username], function (data) { expect(data).to.include('500 Error: invalid userdata'); expect(lastAuth).to.equal(false); done(); }); }); }); describe('Mail tests', function () { let PORT; let connection; let server; beforeEach(function (done) { server = new SMTPServer({