├── LICENSE ├── README.md ├── assets ├── bootstrap.min.css ├── bootstrap.min.js ├── jquery.min.js ├── mailrouter.css └── mailrouter.js ├── bindata.go ├── config.go ├── filter.go ├── filter_test.go ├── helpers.go ├── logs.go ├── logs_test.go ├── mailrouter.go ├── pidfile.go ├── route.go ├── stats.go ├── stats_test.go └── views ├── filters.html ├── index.html └── routes.html /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mark Hale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mailrouter 2 | 3 | A dynamically configurable mail router with a web interface, written in Go. It requires Go 1.2 or newer. 4 | 5 | It is intended to manage and redirect mail sent by deployed applications without having to alter the application or the production mail server configuration. 6 | 7 | ## Features 8 | 9 | * Define filters (routing rules) on From address, To address, Subject header and originating IP. 10 | * Ordering of filters. 11 | * The ability to readdress mail matching a filter. 12 | * A web interface for configuring SMTP routes and routing rules (called filters). 13 | * A customisable listening address and port for both HTTP and SMTP interfaces. 14 | * Logging of delivered and dropped mail messages. 15 | * The ability to set a default route for mail. 16 | * A human-readable configuration file in JSON format. 17 | * IPV6 support, including theoretical use as a gateway for IPV6-only servers to route mail to an IPV4-only mail server. 18 | * Routing loops are allowed. 19 | 20 | ## Intended Use 21 | 22 | Mailrouter is intended to be placed between mail-sending applications and proper SMTP servers. It should then be used to route, forward or drop mail messages as defined by filters on a dynamic basis. 23 | 24 | For example: An organisation has a production mail server mail.example.com, a deployment of the [Mailcatcher](http://mailcatcher.me/) software at mc.example.com, and two versions of a deployed web application that can send user-defined emails. Two development versions of the application run on 10.0.0.1 and 10.0.0.2. A production version runs on 10.0.1.1. 25 | 26 | The organisation could set up: 27 | 28 | * A Route named Outbound to mail.example.com. All mail sent to this Route will be delivered to mail.example.com. 29 | * A Route named QA to mail.example.com with the To field set to qa@example.com. All mail sent to this Route will be delivered to qa@example.com on the mail server, acting as an automatic forward. This will allow the QA team to examine mail messages as they would be received by regular users. 30 | * A route named Mailcatcher to mc.example.com. 31 | 32 | It could then set up the following: 33 | 34 | * A Filter named Development with the Originating IP of 10.0.0.0/24 to route mail to the Mailcatcher Route. This would ensure no mail sent from the 10.0.0.1 server, or any other server in the 10.0.0.x IP range, would escape to the public internet. 35 | * A Filter named Test Mail with the Originating IP of 10.0.1.1 and the To address of "test" to route mail to the QA Route. This would ensure any accounts on the production server with a To address containing the word "test", in either the user or domain section, would be redirected to the QA team. 36 | * The Outbound route as the default route. 37 | 38 | Soon, the application gains new users. However, some of these users are unpleasant, and the organisation adds a Filter with a Subject containing the word "badsite.com" to route mail to the Drop Route. This would drop all mail with links to badsite.com because email from those accounts is to be ignored. 39 | 40 | Later, when the application is no longer under active development, the Route for the Test Mail filter can be set to Drop, as the QA team has been reassigned to the next big thing and no longer needs testing messages from the application. 41 | 42 | ## Compiling 43 | 44 | First get the dependencies: 45 | 46 | go get github.com/mhale/smtpd 47 | go get github.com/streadway/simpleuuid 48 | go get github.com/jteeuwen/go-bindata 49 | 50 | Then install each of them with: 51 | 52 | go install 53 | 54 | ## Command Line Usage 55 | 56 | There are four command line options. 57 | 58 | * -h prints the help message 59 | * -conf specifies the path to store the configuration file. The default is /etc/mailrouter.conf. 60 | * -http specifies an address & port for the HTTP server to listen on. The default is all addresses and port 8080. 61 | * -smtp specifies an address & port for the SMTP server to listen on. The default is all addresses and port 2525. 62 | 63 | The default values are equivalent to: 64 | 65 | mailrouter -conf=/etc/mailrouter.conf -http=:8080 -smtp=:2525 66 | 67 | To bind to a specified IPv4 address: 68 | 69 | mailrouter -http=127.0.0.1:80 -smtp=127.0.0.1:25 70 | 71 | Any IP:port format accepted by Go will work, however IPv6 addresses have not been tested yet. 72 | 73 | ## Tips 74 | 75 | * Create Routes first, so the drop-down Route selector is populated when Filters are created. 76 | * Define Filters in order beginning at 100, numbering the second Filter as 200, the third as 300, and so on. This provides flexibility later when inserting new Filters between existing Filters. 77 | * Filter fields are logical AND operations i.e. they must all match for the Filter to match. Place more specific Filters before general Filters. 78 | * Filters will be checked in the order displayed on the Filters page. 79 | * If no routes are configured, all mail will be dropped. This can be useful when your application requires a mail gateway but you don't care about the mail. 80 | 81 | ## To Do 82 | 83 | * SMTP authentication support. 84 | * SSL/TLS support. 85 | * Verify that use as an IPv4 to IPv6 bridge works. 86 | * Hostname support in the Originating IP field. 87 | * Mail header matching and overriding. 88 | * Filtering by body text. 89 | * Filtering by attachments: file count, file size, MIME type, etc. 90 | * Full end-to-end testing. Currently only basic testing of the filtering functionality is implemented. 91 | 92 | ## Development 93 | 94 | Pull requests are welcome. To edit the assets or views, you will need to get the [go-bindata](https://github.com/jteeuwen/go-bindata/) command line tool (which comes with the package) to generate the bindata.go file: 95 | 96 | cd $GOPATH/src/github.com/jteeuwen/go-bindata/go-bindata 97 | go install 98 | 99 | Then generate a shim with: 100 | 101 | go-bindata -debug assets views 102 | 103 | This will allow you to update the views and hit Refresh in your browser to see your changes without recompiling or restarting the program. 104 | 105 | ## Bugs / Issues 106 | 107 | Please report any bugs you find as Github Issues. 108 | 109 | ## Licensing 110 | 111 | Copyright © 2014 Mark Hale. This program is released under the MIT License. See the LICENSE file for details. 112 | -------------------------------------------------------------------------------- /assets/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('