UNPKG

emailjs-smtp-client

Version:

SMTP Client allows you to connect to an SMTP server in JS.

924 lines (695 loc) 23.8 kB
/* eslint-disable no-unused-expressions */ import SmtpClient from './client' import { LOG_LEVEL_NONE } from './common' describe('smtpclient unit tests', function () { let smtp let host, port, options let openStub, socketStub let TCPSocket beforeEach(function () { host = '127.0.0.1' port = 10000 options = { useSecureTransport: true, ca: 'WOW. SUCH CERT. MUCH TLS.' } smtp = new SmtpClient(host, port, options) smtp.logLevel = LOG_LEVEL_NONE expect(smtp).to.exist TCPSocket = function () { } TCPSocket.open = function () { } TCPSocket.prototype.close = function () { } TCPSocket.prototype.send = function () { } TCPSocket.prototype.suspend = function () { } TCPSocket.prototype.resume = function () { } TCPSocket.prototype.send = function () { } TCPSocket.prototype.upgradeToSecure = function () { } socketStub = sinon.createStubInstance(TCPSocket) openStub = sinon.stub(TCPSocket, 'open').withArgs(host, port).returns(socketStub) smtp.connect(TCPSocket) expect(openStub.callCount).to.equal(1) expect(socketStub.onopen).to.exist expect(socketStub.onerror).to.exist }) describe('tcp-socket websocket proxy', function () { it('should send hostname in onopen', function () { socketStub.onopen({ data: { proxyHostname: 'hostname.io' // hostname of the socket.io proxy in tcp-socket } }) expect(smtp.options.name).to.equal('hostname.io') }) }) describe('#connect', function () { it('should not throw', function () { let client = new SmtpClient(host, port) TCPSocket = { open: function () { let socket = { onopen: function () { }, onerror: function () { } } // disallow setting new properties (eg. oncert) Object.preventExtensions(socket) return socket } } client.connect(TCPSocket) }) }) describe('#suspend', function () { it('should call suspend', function () { socketStub.readyState = 'open' smtp.suspend() expect(socketStub.suspend.callCount).to.equal(1) }) }) describe('#resume', function () { it('should call resume', function () { socketStub.readyState = 'open' smtp.resume() expect(socketStub.resume.callCount).to.equal(1) }) }) describe('#quit', function () { it('should send QUIT', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.quit() expect(_sendCommandStub.withArgs('QUIT').callCount).to.equal(1) _sendCommandStub.restore() }) }) describe('#reset', function () { it('should send RSET', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.reset() expect(_sendCommandStub.withArgs('RSET').callCount).to.equal(1) _sendCommandStub.restore() }) it('should use default authentication', function () { smtp.options.auth = { user: '1' } smtp.reset() expect(smtp.options.auth).to.deep.equal({ user: '1' }) }) it('should store custom authentication', function () { let auth = { user: 'test' } smtp.options.auth = { user: '1' } smtp.reset(auth) expect(smtp.options.auth).to.deep.equal(auth) }) }) describe('#close', function () { it('should close socket', function () { socketStub.readyState = 'open' smtp.close() expect(socketStub.close.callCount).to.equal(1) }) it('should call _destroy', function () { sinon.stub(smtp, '_destroy') socketStub.readyState = '' smtp.close() expect(smtp._destroy.callCount).to.equal(1) smtp._destroy.restore() }) }) describe('#useEnvelope', function () { it('should send MAIL FROM', function () { let envelope = { from: 'ft', to: ['tt'] } let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.useEnvelope(envelope) expect(_sendCommandStub.withArgs('MAIL FROM:<ft>').callCount).to.equal(1) expect(smtp._envelope.from).to.deep.equal(envelope.from) expect(smtp._envelope.to).to.deep.equal(envelope.to) _sendCommandStub.restore() }) }) describe('#send', function () { it('should do nothing if not data mode', function () { smtp._dataMode = false smtp.send() expect(socketStub.send.callCount).to.equal(0) }) it('should send data to socket', function () { let _sendStringStub = sinon.stub(smtp, '_sendString') smtp._dataMode = true smtp.send('abcde') expect(_sendStringStub.withArgs('abcde').callCount).to.equal(1) _sendStringStub.restore() }) }) describe('#end', function () { it('should do nothing if not data mode', function () { smtp._dataMode = false smtp.send() expect(socketStub.send.callCount).to.equal(0) }) it('should send a dot in a separate line', function () { smtp._dataMode = true smtp.end() expect(socketStub.send.callCount).to.equal(1) expect(socketStub.send.args[0][0]).to.deep.equal( new Uint8Array([13, 10, 46, 13, 10]).buffer) // \r\n.\r\n }) }) describe('#_onData', function () { it('should decode and send chunk to parser', function () { let _parserSendStub = sinon.stub(smtp._parser, 'send') smtp._onData({ data: new Uint8Array([97, 98, 99]).buffer // abc }) expect(_parserSendStub.withArgs('abc').callCount).to.equal(1) _parserSendStub.restore() }) }) describe('#_onDrain', function () { it('should emit ondrain', function () { let _ondrainStub = sinon.stub(smtp, 'ondrain') smtp._onDrain() expect(_ondrainStub.callCount).to.equal(1) _ondrainStub.restore() }) }) describe('#_onError', function () { it('should emit onerror and close connection', function () { let _onerrorStub = sinon.stub(smtp, 'onerror') let _closeStub = sinon.stub(smtp, 'close') let err = new Error('abc') smtp._onError({ data: err }) expect(_onerrorStub.withArgs(err).callCount).to.equal(1) expect(_closeStub.callCount).to.equal(1) _onerrorStub.restore() _closeStub.restore() }) }) describe('#_onClose', function () { it('should call _destroy', function () { let _destroyStub = sinon.stub(smtp, '_destroy') smtp._onClose() expect(_destroyStub.callCount).to.equal(1) _destroyStub.restore() }) }) describe('#_onCommand', function () { it('should run stored handler', function () { let _commandStub = sinon.stub() let cmd = 'abc' smtp._currentAction = _commandStub smtp._onCommand(cmd) expect(_commandStub.withArgs(cmd).callCount).to.equal(1) }) }) describe('#_destroy', function () { it('should do nothing if already destroyed', function () { let _oncloseStub = sinon.stub(smtp, 'onclose') smtp.destroyed = true smtp._destroy() expect(_oncloseStub.callCount).to.equal(0) _oncloseStub.restore() }) it('should emit onclose if not destroyed yet', function () { let _oncloseStub = sinon.stub(smtp, 'onclose') smtp.destroyed = false smtp._destroy() expect(_oncloseStub.callCount).to.equal(1) _oncloseStub.restore() }) }) describe('#_sendCommand', function () { it('should convert string to ArrayBuffer and send to socket', function () { smtp._sendCommand('abc') expect(socketStub.send.args[0][0]).to.deep.equal( new Uint8Array([97, 98, 99, 13, 10]).buffer) // abc\r\n }) }) describe('_authenticateUser', function () { it('should emit onidle if no auth info', function () { let _onidleStub = sinon.stub(smtp, 'onidle') smtp.options.auth = false smtp._authenticateUser() expect(_onidleStub.callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionIdle) _onidleStub.restore() }) it('should use AUTH PLAIN by default', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.auth = { user: 'abc', pass: 'def' } smtp._supportedAuth = [] smtp._authenticateUser() expect(_sendCommandStub.withArgs('AUTH PLAIN AGFiYwBkZWY=').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) _sendCommandStub.restore() }) it('should use AUTH LOGIN if specified', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.auth = { user: 'abc', pass: 'def' } smtp._supportedAuth = [] smtp.options.authMethod = 'LOGIN' smtp._authenticateUser() expect(_sendCommandStub.withArgs('AUTH LOGIN').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTH_LOGIN_USER) _sendCommandStub.restore() }) it('should use AUTH XOAUTH2 if specified', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.auth = { user: 'abc', xoauth2: 'def' } smtp._supportedAuth = ['XOAUTH2'] smtp._authenticateUser() expect(_sendCommandStub.withArgs('AUTH XOAUTH2 dXNlcj1hYmMBYXV0aD1CZWFyZXIgZGVmAQE=').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTH_XOAUTH2) _sendCommandStub.restore() }) }) describe('#_actionGreeting', function () { it('should fail if response is not 220', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionGreeting({ statusCode: 500, data: 'test' }) expect(_onErrorStub.calledOnce).to.be.true expect(_onErrorStub.args[0][0].message).to.deep.equal('Invalid greeting: test') _onErrorStub.restore() }) it('should send EHLO on greeting', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.name = 'abc' smtp._actionGreeting({ statusCode: 220, data: 'test' }) expect(_sendCommandStub.withArgs('EHLO abc').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionEHLO) _sendCommandStub.restore() }) it('should send LHLO on greeting', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.name = 'abc' smtp.options.lmtp = true smtp._actionGreeting({ statusCode: 220, data: 'test' }) expect(_sendCommandStub.withArgs('LHLO abc').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionLHLO) _sendCommandStub.restore() }) }) describe('#_actionLHLO', function () { it('should proceed to EHLO', function () { let _actionEHLOStub = sinon.stub(smtp, '_actionEHLO') smtp.options.name = 'abc' smtp._actionLHLO({ success: true, line: '250-AUTH PLAIN LOGIN' }) expect(_actionEHLOStub.callCount).to.equal(1) _actionEHLOStub.restore() }) }) describe('#_actionEHLO', function () { it('should fallback to HELO on error', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.name = 'abc' smtp._actionEHLO({ success: false }) expect(_sendCommandStub.withArgs('HELO abc').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionHELO) _sendCommandStub.restore() }) it('should proceed to authentication', function () { let _authenticateUserStub = sinon.stub(smtp, '_authenticateUser') smtp._actionEHLO({ success: true, line: '250-AUTH PLAIN LOGIN' }) expect(_authenticateUserStub.callCount).to.equal(1) expect(smtp._supportedAuth).to.deep.equal(['PLAIN', 'LOGIN']) _authenticateUserStub.restore() }) it('should proceed to starttls', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp._secureMode = false smtp._actionEHLO({ success: true, line: '250-STARTTLS' }) expect(_sendCommandStub.withArgs('STARTTLS').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionSTARTTLS) _sendCommandStub.restore() }) }) describe('#_actionHELO', function () { it('should proceed to authentication', function () { let _authenticateUserStub = sinon.stub(smtp, '_authenticateUser') smtp._actionHELO({ success: true }) expect(_authenticateUserStub.callCount).to.equal(1) _authenticateUserStub.restore() }) }) describe('#_actionSTARTTLS', function () { it('should upgrade connection', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.name = 'abc' smtp._actionSTARTTLS({ success: true, line: '220 Ready to start TLS' }) expect(smtp.socket.upgradeToSecure.callCount).to.equal(1) expect(_sendCommandStub.withArgs('EHLO abc').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionEHLO) _sendCommandStub.restore() }) }) describe('#_actionAUTH_LOGIN_USER', function () { it('should emit error on invalid input', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionAUTH_LOGIN_USER({ statusCode: 334, // valid status code data: 'test' // invalid value }) expect(_onErrorStub.callCount).to.equal(1) expect(_onErrorStub.args[0][0] instanceof Error).to.be.true _onErrorStub.restore() }) it('should respond to server with base64 encoded username', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.auth = { user: 'abc', pass: 'def' } smtp._actionAUTH_LOGIN_USER({ statusCode: 334, data: 'VXNlcm5hbWU6' }) expect(_sendCommandStub.withArgs('YWJj').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTH_LOGIN_PASS) _sendCommandStub.restore() }) }) describe('#_actionAUTH_LOGIN_PASS', function () { it('should emit error on invalid input', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionAUTH_LOGIN_PASS({ statusCode: 334, // valid status code data: 'test' // invalid value }) expect(_onErrorStub.callCount).to.equal(1) expect(_onErrorStub.args[0][0] instanceof Error).to.be.true _onErrorStub.restore() }) it('should respond to server with base64 encoded password', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp.options.auth = { user: 'abc', pass: 'def' } smtp._actionAUTH_LOGIN_PASS({ statusCode: 334, data: 'UGFzc3dvcmQ6' }) expect(_sendCommandStub.withArgs('ZGVm').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) _sendCommandStub.restore() }) }) describe('#_actionAUTH_XOAUTH2', function () { it('should send empty response on error', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp._actionAUTH_XOAUTH2({ success: false }) expect(_sendCommandStub.withArgs('').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) _sendCommandStub.restore() }) it('should run _actionAUTHComplete on success', function () { let _actionAUTHCompleteStub = sinon.stub(smtp, '_actionAUTHComplete') let cmd = { success: true } smtp._actionAUTH_XOAUTH2(cmd) expect(_actionAUTHCompleteStub.withArgs(cmd).callCount).to.equal(1) _actionAUTHCompleteStub.restore() }) }) describe('#_actionAUTHComplete', function () { it('should emit error on invalid auth', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionAUTHComplete({ success: false, data: 'err' }) expect(_onErrorStub.callCount).to.equal(1) expect(_onErrorStub.args[0][0] instanceof Error).to.be.true _onErrorStub.restore() }) it('should emit idle if auth succeeded', function () { let _onidleStub = sinon.stub(smtp, 'onidle') smtp.options.auth = { user: 'abc', pass: 'def' } smtp._actionAUTHComplete({ success: true }) expect(_onidleStub.callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionIdle) expect(smtp._authenticatedAs).to.equal('abc') _onidleStub.restore() }) }) describe('#_actionMAIL', function () { it('should emit error on invalid input', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionMAIL({ success: false, data: 'err' }) expect(_onErrorStub.calledOnce).to.be.true expect(_onErrorStub.args[0][0].message).to.equal('err') _onErrorStub.restore() }) it('should emit error on empty recipient queue', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._envelope = { rcptQueue: [] } smtp._actionMAIL({ success: true }) expect(_onErrorStub.callCount).to.equal(1) expect(_onErrorStub.args[0][0] instanceof Error).to.be.true _onErrorStub.restore() }) it('should send to the next recipient in queue', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp._envelope = { rcptQueue: ['receiver'] } smtp._actionMAIL({ success: true }) expect(_sendCommandStub.withArgs('RCPT TO:<receiver>').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionRCPT) _sendCommandStub.restore() }) }) describe('#_actionRCPT', function () { it('should send DATA if queue is processed', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp._envelope = { to: ['abc'], rcptFailed: [], rcptQueue: [], responseQueue: [] } smtp._actionRCPT({ success: true }) expect(_sendCommandStub.withArgs('DATA').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionDATA) _sendCommandStub.restore() }) it('should send rerun RCPT if queue is not empty', function () { let _sendCommandStub = sinon.stub(smtp, '_sendCommand') smtp._envelope = { rcptQueue: ['receiver'], responseQueue: [] } smtp._actionRCPT({ success: true }) expect(_sendCommandStub.withArgs('RCPT TO:<receiver>').callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionRCPT) _sendCommandStub.restore() }) it('should emit error if all recipients failed', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._envelope = { to: ['abc'], rcptFailed: ['abc'], rcptQueue: [], responseQueue: [] } smtp._actionRCPT({ success: true }) expect(_onErrorStub.callCount).to.equal(1) expect(_onErrorStub.args[0][0] instanceof Error).to.be.true _onErrorStub.restore() }) }) describe('#_actionRSET', function () { it('should emit error on invalid input', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionRSET({ success: false, data: 'err' }) expect(_onErrorStub.calledOnce).to.be.true expect(_onErrorStub.args[0][0].message).to.equal('err') _onErrorStub.restore() }) it('should proceed to authentication', function () { let _authenticateUserStub = sinon.stub(smtp, '_authenticateUser') smtp._actionRSET({ success: true }) expect(_authenticateUserStub.callCount).to.equal(1) expect(smtp._authenticatedAs).to.be.null _authenticateUserStub.restore() }) }) describe('#_actionDATA', function () { it('should emit error on invalid input', function () { let _onErrorStub = sinon.stub(smtp, '_onError') smtp._actionDATA({ statusCode: 500, data: 'err' }) expect(_onErrorStub.calledOnce).to.be.true expect(_onErrorStub.args[0][0].message).to.equal('err') _onErrorStub.restore() }) it('should emit onready on success', function () { let _onreadyStub = sinon.stub(smtp, 'onready') smtp._envelope = { to: ['abc'], rcptFailed: ['abc'], rcptQueue: [] } smtp._actionDATA({ statusCode: 250 }) expect(_onreadyStub.withArgs(['abc']).callCount).to.equal(1) expect(smtp._currentAction).to.equal(smtp._actionIdle) expect(smtp._dataMode).to.be.true _onreadyStub.restore() }) }) describe('#_actionStream', function () { it('should emit ondone with argument false', function () { let _ondoneStub = sinon.stub(smtp, 'ondone') smtp._actionStream({ success: false }) expect(_ondoneStub.withArgs(false).callCount).to.equal(1) _ondoneStub.restore() }) it('should emit ondone with argument true', function () { let _ondoneStub = sinon.stub(smtp, 'ondone') smtp._actionStream({ success: true }) expect(_ondoneStub.withArgs(true).callCount).to.equal(1) _ondoneStub.restore() }) it('should emit onidle if required', function () { let _onidleStub = sinon.stub(smtp, 'onidle') smtp._currentAction = smtp._actionIdle smtp._actionStream({ success: true }) expect(_onidleStub.callCount).to.equal(1) _onidleStub.restore() }) it('should cancel onidle', function () { let _onidleStub = sinon.stub(smtp, 'onidle') smtp.ondone = function () { this._currentAction = false } smtp._actionStream({ success: true }) expect(_onidleStub.callCount).to.equal(0) _onidleStub.restore() }) describe('LMTP responses', function () { it('should receive single responses', function () { let _ondoneStub = sinon.stub(smtp, 'ondone') smtp.options.lmtp = true smtp._envelope = { responseQueue: ['abc'], rcptFailed: [] } smtp._actionStream({ success: false }) expect(_ondoneStub.withArgs(true).callCount).to.equal(1) expect(smtp._envelope.rcptFailed).to.deep.equal(['abc']) _ondoneStub.restore() }) it('should wait for additional responses', function () { let _ondoneStub = sinon.stub(smtp, 'ondone') smtp.options.lmtp = true smtp._envelope = { responseQueue: ['abc', 'def', 'ghi'], rcptFailed: [] } smtp._actionStream({ success: false }) smtp._actionStream({ success: true }) smtp._actionStream({ success: false }) expect(_ondoneStub.withArgs(true).callCount).to.equal(1) expect(smtp._envelope.rcptFailed).to.deep.equal(['abc', 'ghi']) _ondoneStub.restore() }) }) }) describe('#_buildXOAuth2Token', function () { it('should return base64 encoded XOAUTH2 token', function () { expect(smtp._buildXOAuth2Token('user@host', 'abcde')).to.equal('dXNlcj11c2VyQGhvc3QBYXV0aD1CZWFyZXIgYWJjZGUBAQ==') }) }) })