├── .npmignore ├── .travis.yml ├── README.md ├── .gitignore ├── package.json ├── LICENSE ├── lib └── index.js └── test └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | !.npmignore 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "9" 6 | - "node" 7 | 8 | sudo: false 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailback 2 | 3 | An SMTP server used to receive incoming emails and pass them as callbacks for testing email 4 | related services. 5 | 6 | [![Build Status](https://secure.travis-ci.org/hueniverse/mailback.png)](http://travis-ci.org/hueniverse/mailback) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | config.json 9 | .DS_Store 10 | */.DS_Store 11 | */*/.DS_Store 12 | ._* 13 | */._* 14 | */*/._* 15 | coverage.* 16 | .settings 17 | package-lock.json 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailback", 3 | "description": "SMTP Server with callback for testing", 4 | "version": "2.0.0", 5 | "repository": "git://github.com/hueniverse/mailback", 6 | "main": "lib/index.js", 7 | "keywords": [ 8 | "smtp", 9 | "server", 10 | "test" 11 | ], 12 | "engines": { 13 | "node": ">=8.9.0" 14 | }, 15 | "dependencies": { 16 | "hoek": "5.x.x", 17 | "joi": "13.x.x", 18 | "mailparser": "2.x.x", 19 | "smtp-server": "3.x.x", 20 | "teamwork": "3.x.x", 21 | "wreck": "14.x.x" 22 | }, 23 | "devDependencies": { 24 | "code": "5.x.x", 25 | "lab": "15.x.x", 26 | "nodemailer": "4.x.x" 27 | }, 28 | "scripts": { 29 | "test": "lab -a code -t 100 -L", 30 | "test-cov-html": "lab -a code -r html -o coverage.html" 31 | }, 32 | "license": "BSD-3-Clause" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017, Eran Hammer and Project contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of any contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | * * * 27 | 28 | The complete list of contributors can be found at: https://github.com/hueniverse/mailback/graphs/contributors 29 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Hoek = require('hoek'); 6 | const Joi = require('joi'); 7 | const MailParser = require('mailparser'); 8 | const SmtpServer = require('smtp-server'); 9 | const Teamwork = require('teamwork'); 10 | const Wreck = require('wreck'); 11 | 12 | 13 | // Declare internals 14 | 15 | const internals = {}; 16 | 17 | 18 | internals.schema = Joi.object({ 19 | host: Joi.string().default('0.0.0.0'), 20 | port: Joi.number().integer().min(0).default(0), 21 | onMessage: Joi.func().required(), 22 | smtp: Joi.object({ 23 | onData: Joi.forbidden() 24 | }) 25 | .unknown() 26 | }); 27 | 28 | 29 | exports.Server = internals.Server = function (options) { 30 | 31 | options = Joi.attempt(options, internals.schema, 'Invalid server options'); 32 | this._settings = Hoek.clone(options); 33 | this._settings.smtp = this._settings.smtp || {}; 34 | this._settings.smtp.disabledCommands = this._settings.smtp.disabledCommands || ['AUTH']; 35 | this._settings.smtp.logger = (this._settings.smtp.logger !== undefined ? this._settings.smtp.logger : false); 36 | this._settings.smtp.onData = (stream, session, callback) => this._onMessage(stream, session, callback); 37 | 38 | this._server = new SmtpServer.SMTPServer(this._settings.smtp); 39 | }; 40 | 41 | 42 | internals.Server.prototype.start = async function () { 43 | 44 | const team = new Teamwork(); 45 | this._server.listen(this._settings.port, this._settings.host, () => team.attend()); 46 | await team.work; 47 | 48 | const address = this._server.server.address(); 49 | this.info = { 50 | address: address.address, 51 | port: address.port 52 | }; 53 | }; 54 | 55 | 56 | internals.Server.prototype.stop = async function () { 57 | 58 | const team = new Teamwork(); 59 | this._server.close(() => team.attend()); 60 | await team.work; 61 | }; 62 | 63 | 64 | internals.Server.prototype._onMessage = async function (stream, session, callback) { 65 | 66 | try { 67 | const message = await Wreck.read(stream); 68 | const mail = await MailParser.simpleParser(message); 69 | mail.from = mail.from.value; 70 | mail.to = mail.to.value; 71 | this._settings.onMessage(null, mail); 72 | } 73 | catch (err) { 74 | this._settings.onMessage(err); 75 | } 76 | 77 | return callback(); 78 | }; 79 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Lab = require('lab'); 7 | const Mailback = require('..'); 8 | const MailParser = require('mailparser'); 9 | const Nodemailer = require('nodemailer'); 10 | const Wreck = require('wreck'); 11 | 12 | 13 | // Declare internals 14 | 15 | const internals = {}; 16 | 17 | 18 | // Test shortcuts 19 | 20 | const { describe, it } = exports.lab = Lab.script(); 21 | const expect = Code.expect; 22 | 23 | 24 | describe('Server', () => { 25 | 26 | it('calls back on new message', async () => { 27 | 28 | const onMessage = (err, message) => { 29 | 30 | expect(err).to.not.exist(); 31 | expect(message.from).to.equal([{ address: 'test@example.com', name: 'test' }]); 32 | expect(message.text).to.equal('I got something to tell you\n'); 33 | expect(message.subject).to.equal('hello'); 34 | expect(message.to).to.equal([{ address: 'someone@example.com', name: 'someone' }]); 35 | }; 36 | 37 | const server = new Mailback.Server({ onMessage }); 38 | await server.start(); 39 | await internals.email(server); 40 | await server.stop(); 41 | }); 42 | 43 | it('overrides defaults', async () => { 44 | 45 | const onMessage = (err, message) => { 46 | 47 | expect(err).to.not.exist(); 48 | expect(message.from).to.equal([{ address: 'test@example.com', name: 'test' }]); 49 | expect(message.text).to.equal('I got something to tell you\n'); 50 | expect(message.subject).to.equal('hello'); 51 | expect(message.to).to.equal([{ address: 'someone@example.com', name: 'someone' }]); 52 | }; 53 | 54 | const server = new Mailback.Server({ 55 | onMessage, 56 | smtp: { 57 | logger: false, 58 | disabledCommands: ['AUTH'] 59 | } 60 | }); 61 | 62 | await server.start(); 63 | await internals.email(server); 64 | await server.stop(); 65 | }); 66 | 67 | it('errors on stream error', async () => { 68 | 69 | const orig = Wreck.read; 70 | Wreck.read = (stream, options, next) => { 71 | 72 | Wreck.read = orig; 73 | Wreck.read(stream, options, (errIgnore, message) => next(new Error())); 74 | }; 75 | 76 | const onMessage = (err, message) => { 77 | 78 | expect(err).to.exist(); 79 | }; 80 | 81 | const server = new Mailback.Server({ onMessage }); 82 | await server.start(); 83 | await internals.email(server); 84 | await server.stop(); 85 | }); 86 | 87 | it('errors on parser error', async () => { 88 | 89 | const orig = MailParser.simpleParser; 90 | MailParser.simpleParser = () => { 91 | 92 | MailParser.simpleParser = orig; 93 | return Promise.reject(new Error()); 94 | }; 95 | 96 | const onMessage = (err, message) => { 97 | 98 | expect(err).to.exist(); 99 | }; 100 | 101 | const server = new Mailback.Server({ onMessage }); 102 | await server.start(); 103 | await internals.email(server); 104 | await server.stop(); 105 | }); 106 | }); 107 | 108 | 109 | internals.email = function (server) { 110 | 111 | const mail = { 112 | from: 'test ', 113 | to: 'someone ', 114 | subject: 'hello', 115 | text: 'I got something to tell you' 116 | }; 117 | 118 | const transporter = Nodemailer.createTransport({ host: server.info.host, port: server.info.port, secure: false, ignoreTLS: true }); 119 | return transporter.sendMail(mail); 120 | }; 121 | --------------------------------------------------------------------------------