├── config ├── databytes ├── loglevel ├── me ├── rdns.allow_regexps ├── rdns.deny_regexps ├── early_talker.pause ├── max_unrecognized_commands ├── data.uribl.zones ├── dnsbl.zones ├── lookup_rdns.strict.timeout ├── cluster_modules ├── auth_flat_file.ini ├── smtp_forward.ini ├── smtp_proxy.ini ├── host_list ├── rcpt_to.blocklist ├── lookup_rdns.strict.whitelist ├── host_list_regex ├── lookup_rdns.strict.ini ├── lookup_rdns.strict.whitelist_regex ├── smtp.ini ├── helo.checks.ini └── plugins ├── docs ├── plugins │ ├── spamassassin.md │ ├── queue │ │ ├── deliver.md │ │ ├── qmail-queue.md │ │ ├── smtp_forward.md │ │ └── smtp_proxy.md │ ├── mail_from.is_resolvable.md │ ├── rcpt_to.blocklist.md │ ├── mail_from.blocklist.md │ ├── dnsbl.md │ ├── data.nomsgid.md │ ├── rcpt_to.max_count.md │ ├── mail_from.nobounces.md │ ├── data.noreceived.md │ ├── data.signatures.md │ ├── tls.md │ ├── early_talker.md │ ├── max_unrecognized_commands.md │ ├── graph.md │ ├── relay_all.md │ ├── auth │ │ └── flat_file.md │ ├── rcpt_to.in_host_list.md │ ├── data.uribl.md │ ├── block_me.md │ ├── rdns.regexp.md │ ├── helo.checks.md │ └── lookup_rdns.strict.md ├── Body.md ├── Connection.md ├── Header.md ├── Address.md ├── Transaction.md ├── Config.md ├── CoreConfig.md ├── Plugins.md ├── Tutorial.md └── CustomReturnCodes.md ├── TODO ├── constants.js ├── plugins ├── data.nomsgid.js ├── mail_from.nobounces.js ├── relay_all.js ├── mail_from.blocklist.js ├── rcpt_to.blocklist.js ├── test_queue.js ├── queue │ ├── deliver.js │ ├── qmail-queue.js │ ├── smtp_forward.js │ └── smtp_proxy.js ├── data.noreceived.js ├── rcpt_to.max_count.js ├── early_talker.js ├── max_unrecognized_commands.js ├── data.signatures.js ├── mail_from.is_resolvable.js ├── rdns.regexp.js ├── rcpt_to.in_host_list.js ├── tls.js ├── dnsbl.js ├── helo.checks.js ├── auth │ └── flat_file.js ├── block_me.js ├── data.uribl.js ├── lookup_rdns.strict.js ├── spamassassin.js └── graph.js ├── package.json ├── tests ├── rfc1869.js └── address.js ├── line_socket.js ├── haraka.js ├── LICENSE ├── config.js ├── utils.js ├── transaction.js ├── rfc1869.js ├── logger.js ├── configfile.js ├── mailheader.js ├── address.js ├── server.js ├── README.md ├── tls_server.js ├── mailbody.js ├── plugins.js ├── dsn.js └── bin └── haraka /config/databytes: -------------------------------------------------------------------------------- 1 | 500000 2 | -------------------------------------------------------------------------------- /config/loglevel: -------------------------------------------------------------------------------- 1 | LOGDEBUG 2 | -------------------------------------------------------------------------------- /config/me: -------------------------------------------------------------------------------- 1 | haraka.test 2 | -------------------------------------------------------------------------------- /config/rdns.allow_regexps: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/rdns.deny_regexps: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/early_talker.pause: -------------------------------------------------------------------------------- 1 | 5000 2 | -------------------------------------------------------------------------------- /config/max_unrecognized_commands: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /config/data.uribl.zones: -------------------------------------------------------------------------------- 1 | multi.surbl.org 2 | -------------------------------------------------------------------------------- /config/dnsbl.zones: -------------------------------------------------------------------------------- 1 | zen.spamhaus.org 2 | -------------------------------------------------------------------------------- /config/lookup_rdns.strict.timeout: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /config/cluster_modules: -------------------------------------------------------------------------------- 1 | repl:8888 2 | stats 3 | -------------------------------------------------------------------------------- /config/auth_flat_file.ini: -------------------------------------------------------------------------------- 1 | [users] 2 | matt=test 3 | -------------------------------------------------------------------------------- /config/smtp_forward.ini: -------------------------------------------------------------------------------- 1 | host=localhost 2 | port=2555 3 | 4 | -------------------------------------------------------------------------------- /config/smtp_proxy.ini: -------------------------------------------------------------------------------- 1 | host=localhost 2 | port=2555 3 | timeout=30 4 | -------------------------------------------------------------------------------- /config/host_list: -------------------------------------------------------------------------------- 1 | # add hosts in here we want to accept mail for 2 | haraka.local 3 | 4 | -------------------------------------------------------------------------------- /config/rcpt_to.blocklist: -------------------------------------------------------------------------------- 1 | # This is a blocklist for the rcpt_to line. One address per line. 2 | -------------------------------------------------------------------------------- /config/lookup_rdns.strict.whitelist: -------------------------------------------------------------------------------- 1 | # Hostnames and IPs are matched exactly as written on each line. 2 | -------------------------------------------------------------------------------- /docs/plugins/spamassassin.md: -------------------------------------------------------------------------------- 1 | spamassassin 2 | ============ 3 | 4 | This plugin runs the mail through SpamAssassin. 5 | 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - DKIM (need a node dkim library first - perhaps based on libdkimpp?) 2 | - Rate Limiting for outbound mail 3 | - Milter support 4 | -------------------------------------------------------------------------------- /docs/plugins/queue/deliver.md: -------------------------------------------------------------------------------- 1 | queue/deliver 2 | ============= 3 | 4 | This plugin is now redundant. Outbound delivery is now built into Haraka. -------------------------------------------------------------------------------- /docs/plugins/mail_from.is_resolvable.md: -------------------------------------------------------------------------------- 1 | mail_from.is_resolvable 2 | ======================= 3 | 4 | This plugin checks that the domain used in MAIL FROM is resolvable to an MX 5 | record. 6 | -------------------------------------------------------------------------------- /docs/plugins/rcpt_to.blocklist.md: -------------------------------------------------------------------------------- 1 | rcpt_to.blocklist 2 | =================== 3 | 4 | This mail blocks RCPT_TO addresses in a list. 5 | 6 | Configuration 7 | ------------- 8 | 9 | * rcpt_to.blocklist 10 | 11 | Contains a list of email addresses to block. 12 | -------------------------------------------------------------------------------- /docs/plugins/mail_from.blocklist.md: -------------------------------------------------------------------------------- 1 | mail_from.blocklist 2 | =================== 3 | 4 | This mail blocks MAIL_FROM addresses in a list. 5 | 6 | Configuration 7 | ------------- 8 | 9 | * mail_from.blocklist 10 | 11 | Contains a list of email addresses to block. 12 | -------------------------------------------------------------------------------- /docs/plugins/dnsbl.md: -------------------------------------------------------------------------------- 1 | dnsbl 2 | ===== 3 | 4 | This plugin looks up the connecting IP address in an IP blocklist. Mails 5 | found to be in the blocklist are rejected. 6 | 7 | Configuration 8 | ------------- 9 | 10 | * dnsbl.zones 11 | 12 | A list of zones to query. 13 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | 3 | exports.cont = 900; 4 | exports.stop = 901; 5 | exports.deny = 902; 6 | exports.denysoft = 903; 7 | exports.denydisconnect = 904; 8 | exports.disconnect = 905; 9 | exports.ok = 906; 10 | -------------------------------------------------------------------------------- /docs/plugins/data.nomsgid.md: -------------------------------------------------------------------------------- 1 | data.nomsgid 2 | ============ 3 | 4 | Quite simply enabling this plugin blocks all mails lacking a Message-Id 5 | header. This is an aggressive anti-spam measure, but since most mail systems 6 | will add a Message-Id header, it tends to block a good chunk of abusive mail. 7 | -------------------------------------------------------------------------------- /docs/plugins/rcpt_to.max_count.md: -------------------------------------------------------------------------------- 1 | rcpt_to.max_count 2 | ================= 3 | 4 | This plugin sets a maximum limit on RCPT TOs. Violators will be disconnected. 5 | 6 | Configuration 7 | ------------- 8 | 9 | * rcpt_to.max_count 10 | 11 | The maximum number of recipients. Default: 40. 12 | -------------------------------------------------------------------------------- /config/host_list_regex: -------------------------------------------------------------------------------- 1 | # Add regexes in here we want to accept mail for. 2 | # Specifies the list of regexes that are local to this server. Note 3 | # all these regexes are anchored with ^regex$. One can not choose not to 4 | # anchor with .* and that there is a good potential for bad regexes being 5 | # over permissive if we don't do this. 6 | 7 | -------------------------------------------------------------------------------- /docs/plugins/queue/qmail-queue.md: -------------------------------------------------------------------------------- 1 | queue/qmail-queue 2 | ================= 3 | 4 | This plugin delivers the mail to the `qmail-queue` program, which can be used 5 | for both inbound and outbound delivery. 6 | 7 | Configuration 8 | ------------- 9 | 10 | * qmail-queue.path 11 | 12 | The path to the `qmail-queue` binary. Default: `/var/qmail/bin/qmail-queue` 13 | -------------------------------------------------------------------------------- /plugins/data.nomsgid.js: -------------------------------------------------------------------------------- 1 | // Check whether an email has a Message-Id header or not, and reject if not 2 | 3 | exports.hook_data_post = function (next, connection) { 4 | if (connection.transaction.header.get_all('Message-Id').length === 0) { 5 | next(DENY, "Mails here must have a Message-Id header"); 6 | } 7 | else { 8 | next(); 9 | } 10 | } -------------------------------------------------------------------------------- /config/lookup_rdns.strict.ini: -------------------------------------------------------------------------------- 1 | [general] 2 | nomatch=Please setup matching DNS and rDNS records. 3 | timeout=60 4 | timeout_msg=DNS check timed out. 5 | 6 | [forward] 7 | nxdomain=Please setup a forward DNS record. 8 | dnserror=Please setup matching DNS and rDNS records. 9 | 10 | [reverse] 11 | nxdomain=Please setup a reverse DNS record. 12 | dnserror=Please setup matching DNS and rDNS records. 13 | -------------------------------------------------------------------------------- /config/lookup_rdns.strict.whitelist_regex: -------------------------------------------------------------------------------- 1 | # Does the same thing as the whitelist file, but each line is a regex. 2 | # Each line is also anchored for you, meaning '^' + regex + '$' is added for 3 | # you. If you need to get around this restriction, you may use a '.*' at 4 | # either the start or the end of your regex. This should help prevent people 5 | # from writing overly permissive rules on accident. 6 | -------------------------------------------------------------------------------- /plugins/mail_from.nobounces.js: -------------------------------------------------------------------------------- 1 | // I don't allow MAIL FROM:<> on my server, because it's all crap and I send 2 | // so little mail anyway that I rarely get real bounces 3 | 4 | exports.hook_mail = function (next, connection, params) { 5 | var mail_from = params[0]; 6 | if (mail_from.isNull()) { 7 | return next(DENY, "No bounces accepted here"); 8 | } 9 | return next(); 10 | } 11 | -------------------------------------------------------------------------------- /plugins/relay_all.js: -------------------------------------------------------------------------------- 1 | // Just relay everything - could be useful for a spamtrap 2 | 3 | exports.register = function() { 4 | this.register_hook('rcpt', 'confirm_all'); 5 | }; 6 | 7 | exports.confirm_all = function(next, connection, params) { 8 | var recipient = params.shift(); 9 | this.loginfo("confirming recipient " + recipient); 10 | connection.relaying = 1; 11 | next(OK); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/plugins/mail_from.nobounces.md: -------------------------------------------------------------------------------- 1 | mail_from.nobounces 2 | =================== 3 | 4 | This mail blocks all bounce messages using the simple rule of checking 5 | for `MAIL FROM:<>`. 6 | 7 | This is useful to enable if you have a mail server that gets spoofed too 8 | much but very few legitimate users. It is potentially bad to block all 9 | bounce messages, but unfortunately for some hosts, sometimes necessary. 10 | -------------------------------------------------------------------------------- /docs/plugins/data.noreceived.md: -------------------------------------------------------------------------------- 1 | data.noreceived 2 | =============== 3 | 4 | This plugin very simply blocks any mail arriving at your system that has no 5 | `Received` headers. 6 | 7 | This is an aggressive anti-spam measure, but works because all real mail 8 | relays will add a `Received` header according to the RFCs. It may false 9 | positive on some bulk mail that uses a custom tool to send, but this appears 10 | to be fairly rare. 11 | -------------------------------------------------------------------------------- /docs/plugins/data.signatures.md: -------------------------------------------------------------------------------- 1 | data.signatures 2 | =============== 3 | 4 | This plugin allows you to add string signatures to a configuration file and 5 | have this plugin scan the body text of an email for those strings. Mails 6 | matching these signatures will be blocked. 7 | 8 | Configuration 9 | ------------- 10 | 11 | * data.signatures 12 | 13 | This file contains a list of strings (one per line) that will be matched. 14 | 15 | -------------------------------------------------------------------------------- /plugins/mail_from.blocklist.js: -------------------------------------------------------------------------------- 1 | // Block mail from matching anything in the list 2 | var utils = require('./utils'); 3 | 4 | exports.hook_mail = function (next, connection, params) { 5 | var mail_from = params[0].address(); 6 | var list = this.config.get('mail_from.blocklist', 'list'); 7 | if (utils.in_array(mail_from, list)) { 8 | return next(DENY, "Mail from you is not allowed here"); 9 | } 10 | return next(); 11 | } 12 | -------------------------------------------------------------------------------- /plugins/rcpt_to.blocklist.js: -------------------------------------------------------------------------------- 1 | // Block mail from matching anything in the list 2 | var utils = require('./utils'); 3 | 4 | exports.hook_rcpt = function (next, connection, params) { 5 | var rcpt_to = params[0].address(); 6 | var list = this.config.get('rcpt_to.blocklist', 'list'); 7 | if (utils.in_array(rcpt_to, list)) { 8 | return next(DENY, "Mail to " + rcpt_to + "is not allowed here"); 9 | } 10 | return next(); 11 | } 12 | -------------------------------------------------------------------------------- /config/smtp.ini: -------------------------------------------------------------------------------- 1 | ; port to listen on 2 | port=25 3 | 4 | ; address to listen on (default: all addresses) 5 | ;listen_address=0.0.0.0 6 | 7 | ; Time in seconds to let sockets be idle with no activity 8 | ;inactivity_timeout=300 9 | 10 | ; Drop privileges to this user 11 | ;user=smtp 12 | 13 | ; Don't stop Haraka if plugins fail to compile 14 | ;ignore_bad_plugins=0 15 | 16 | ; Run using cluster (if installed: "npm install cluster") 17 | ;nodes=cpus 18 | -------------------------------------------------------------------------------- /plugins/test_queue.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | 4 | exports.hook_queue = function(next, connection) { 5 | var lines = connection.transaction.data_lines; 6 | if (lines.length === 0) { 7 | return next(DENY); 8 | } 9 | 10 | fs.writeFile('/tmp/mail.eml', lines.join(''), function(err) { 11 | if (err) { 12 | return next(DENY, "Saving failed"); 13 | } 14 | 15 | return next(OK); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/plugins/tls.md: -------------------------------------------------------------------------------- 1 | tls 2 | === 3 | 4 | This plugin enables the use of TLS (via `STARTTLS`) in Haraka. 5 | 6 | For this to work you need to create a certificate and key file in the 7 | config directory. To do that use the following command: 8 | 9 | openssl req -x509 -nodes -days 2190 -newkey rsa:1024 \ 10 | -keyout config/tls_key.pem -out config/tls_cert.pem 11 | 12 | You will need to provide some details of location. And make the CN equal to 13 | the contents of your `config/me` file. 14 | -------------------------------------------------------------------------------- /plugins/queue/deliver.js: -------------------------------------------------------------------------------- 1 | // This plugin is now entirely redundant. The core will queue outbound mails 2 | // automatically just like this. It is kept here for backwards compatibility 3 | // purposes only. 4 | 5 | var outbound = require('./outbound'); 6 | 7 | exports.hook_queue_outbound = function (next, connection) { 8 | if (!connection.relaying) { 9 | return next(); // we're not relaying so don't deliver outbound 10 | } 11 | 12 | outbound.send_email(connection.transaction, next); 13 | } 14 | -------------------------------------------------------------------------------- /docs/plugins/early_talker.md: -------------------------------------------------------------------------------- 1 | early_talker 2 | ============ 3 | 4 | This plugin checks for early talkers. These are violators of the SMTP 5 | specification, which demands that clients must wait for responses before 6 | sending the next command. 7 | 8 | This plugin checks for early talkers at the DATA command. 9 | 10 | Configuration 11 | ------------- 12 | 13 | * early_talker.pause 14 | 15 | Specifies a delay in milliseconds to delay at the DATA command before 16 | sending the response, while waiting for early talkers. Default is no pause. -------------------------------------------------------------------------------- /docs/plugins/max_unrecognized_commands.md: -------------------------------------------------------------------------------- 1 | max_unrecognized_commands 2 | ========================= 3 | 4 | This plugin places a maximum limit on the number of unrecognized commands 5 | allowed before recognising that the connection is bad. 6 | 7 | If the limit is reached the connecting client is sent an error message and 8 | immediately (and rudely - technically an RFC violation) disconnected. 9 | 10 | Configuration 11 | ------------- 12 | 13 | * max_unrecognized_commands 14 | 15 | Specifies the number of unrecognised commands to allow before disconnecting. 16 | Default: 10. -------------------------------------------------------------------------------- /plugins/data.noreceived.js: -------------------------------------------------------------------------------- 1 | // Check whether an email has any received headers or not, and reject if not 2 | 3 | // NB: Don't check this on your outbounds. It's also a pretty strict check 4 | // for inbounds too, so use with caution. 5 | 6 | exports.hook_data_post = function (next, connection) { 7 | // We always have the received header that Haraka added, so check for 1 8 | if (connection.transaction.header.get_all('Received').length === 1) { 9 | next(DENY, "Mails here must have a Received header"); 10 | } 11 | else { 12 | next(); 13 | } 14 | } -------------------------------------------------------------------------------- /docs/plugins/graph.md: -------------------------------------------------------------------------------- 1 | graph 2 | ===== 3 | 4 | This plugin logs accepted and rejected emails into a database and provides 5 | a web server which you can browse to and view graphs over time of the 6 | plugins which rejected connections. 7 | 8 | In order for this to work you need to install the `sqlite` module via 9 | `npm install sqlite` in your Haraka directory. 10 | 11 | Configuration 12 | ------------- 13 | 14 | * grapher.http_port 15 | 16 | The port to listen on for http. Default: `8080`. 17 | 18 | * grapher.ignore_re 19 | 20 | Regular expression to match plugins to ignore for logging. 21 | Default: `queue|graph|relay` 22 | -------------------------------------------------------------------------------- /docs/plugins/relay_all.md: -------------------------------------------------------------------------------- 1 | relay_all 2 | ========= 3 | 4 | This plugin is useful in spamtraps to accept mail to any host, and to allow 5 | any user from anywhere to send email. 6 | 7 | Do NOT use this plugin on a real mail server, unless you really know what 8 | you are doing. If you use this plugin with anything that relays mail (such 9 | as forwarding to a real mail server, or the `deliver` plugin), your mail 10 | server is now an open relay. 11 | 12 | This is BAD. Hence the big letters. In short: DO NOT USE THIS PLUGIN. 13 | 14 | It is useful for testing, hence why it is here. Also I work with spamtraps 15 | a lot, so it is useful there. 16 | -------------------------------------------------------------------------------- /docs/Body.md: -------------------------------------------------------------------------------- 1 | Body Object 2 | =========== 3 | 4 | The Body object gives you access to the textual body parts of an email. 5 | 6 | API 7 | --- 8 | 9 | * body.bodytext 10 | 11 | A String containing the body text. Note that HTML parts will have tags in-tact. 12 | 13 | * body.header 14 | 15 | The header of this MIME part. See the `Header Object` for details of the API. 16 | 17 | * body.children 18 | 19 | Any child MIME parts. For example a multipart/alternative mail will have a 20 | main body part with just the MIME preamble in (which is usually either empty, 21 | or reads something like "This is a multipart MIME message"), and two 22 | children, one text/plain and one text/html. 23 | -------------------------------------------------------------------------------- /config/helo.checks.ini: -------------------------------------------------------------------------------- 1 | ; By default we do all checks 2 | ; disable these if you are worried about strictness 3 | 4 | check_no_dot=1 5 | check_dynamic=1 6 | check_raw_ip=1 7 | 8 | [bigco] 9 | msn.com=msn.com 10 | hotmail.com=hotmail.com 11 | yahoo.com=yahoo.com,yahoo.co.jp 12 | yahoo.co.jp=yahoo.com,yahoo.co.jp 13 | yahoo.co.uk=yahoo.co.uk 14 | excite.com=excite.com,excitenetwork.com 15 | mailexcite.com=excite.com,excitenetwork.com 16 | yahoo.co.jp=yahoo.com,yahoo.co.jp 17 | mailexcite.com=excite.com,excitenetwork.com 18 | aol.com=aol.com 19 | compuserve.com=compuserve.com,adelphia.net 20 | nortelnetworks.com=nortelnetworks.com,nortel.com 21 | earthlink.net=earthlink.net 22 | earthling.net=earthling.net 23 | -------------------------------------------------------------------------------- /config/plugins: -------------------------------------------------------------------------------- 1 | # default list of plugins 2 | 3 | # block mails from known bad hosts (see config/dnsbl.zones for the DNS zones queried) 4 | dnsbl 5 | 6 | # allow bad mail signatures from the config/data.signatures file. 7 | data.signatures 8 | 9 | # block mail from some known bad HELOs - see config/helo.checks.ini for configuration 10 | helo.checks 11 | 12 | # block mail from known bad email addresses you put in config/mail_from.blocklist 13 | mail_from.blocklist 14 | 15 | # Only accept mail where the MAIL FROM domain is resolvable to an MX record 16 | mail_from.is_resolvable 17 | 18 | # Only accept mail for your personal list of hosts 19 | rcpt_to.in_host_list 20 | 21 | # Queue mail via qmail-queue 22 | queue/smtp_forward 23 | -------------------------------------------------------------------------------- /plugins/rcpt_to.max_count.js: -------------------------------------------------------------------------------- 1 | // Implement a maximum number of recipients per mail 2 | // this helps guard against some spammers who send RCPT TO a gazillion times 3 | // as a way of probing for a working address 4 | 5 | exports.hook_rcpt = function (next, connection) { 6 | if (connection.transaction.notes.rcpt_to_count) { 7 | connection.transaction.notes.rcpt_to_count++; 8 | } 9 | else { 10 | connection.transaction.notes.rcpt_to_count = 1; 11 | } 12 | 13 | var max_count = this.config.get('rcpt_to.max_count') || 40; 14 | 15 | if (connection.transaction.notes.rcpt_to_count > max_count) { 16 | return next(DENYDISCONNECT, "Too many recipient attempts"); 17 | } 18 | return next(); 19 | }; 20 | -------------------------------------------------------------------------------- /docs/plugins/auth/flat_file.md: -------------------------------------------------------------------------------- 1 | auth/flat_file 2 | ============== 3 | 4 | The `auth/flat_file` plugin allows you to create a file containing username 5 | and password combinations, and have relaying users authenticate from that 6 | file. 7 | 8 | Note that passwords are stored in clear-text, so this may not be a great idea 9 | for large scale systems. However the plugin would be a good start for someone 10 | looking to implement authentication using some other form of auth. 11 | 12 | Configuration 13 | ------------- 14 | 15 | Configuration is stored in `config/auth_flat_file.ini` and uses the INI 16 | style formatting. Users are stored in the `[users]` section. 17 | 18 | Example: 19 | 20 | [users] 21 | user1=password1 22 | user2=password2 23 | -------------------------------------------------------------------------------- /plugins/early_talker.js: -------------------------------------------------------------------------------- 1 | // This plugin checks for clients that talk before we sent a response 2 | 3 | exports.register = function() { 4 | this.register_hook('data', 'check_early_talker'); 5 | }; 6 | 7 | exports.check_early_talker = function(next, connection) { 8 | var pause = this.config.get('early_talker.pause'); 9 | if (pause) { 10 | setTimeout(function () { _check_early_talker(connection, next) }, pause); 11 | } 12 | else { 13 | _check_early_talker(connection, next); 14 | } 15 | }; 16 | 17 | var _check_early_talker = function (connection, next) { 18 | if (connection.early_talker) { 19 | next(DENYDISCONNECT, "You talk too soon"); 20 | } 21 | else { 22 | next(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /docs/plugins/rcpt_to.in_host_list.md: -------------------------------------------------------------------------------- 1 | rcpt_to.in_host_list 2 | ==================== 3 | 4 | This plugin is the mainstay of an inbound Haraka server. It should list the 5 | domains that are local to the host. Mails that have RCPT TO not matching 6 | a host in the given list will be passed onto other rcpt hooks and possibly 7 | rejected. 8 | 9 | Configuration 10 | ------------- 11 | 12 | * host_list 13 | 14 | Specifies the list of hosts that are local to this server. 15 | 16 | * host_list_regex 17 | 18 | Specifies the list of regexes that are local to this server. Note 19 | all these regexes are anchored with ^regex$. One can not choose not to 20 | anchor with .* and that there is a good potential for bad regexes being 21 | over permissive if we don\'t do this. 22 | -------------------------------------------------------------------------------- /docs/plugins/data.uribl.md: -------------------------------------------------------------------------------- 1 | data.uribl 2 | ========== 3 | 4 | This plugin extracts URLs and feeds them to URL based DNS blocklists such 5 | as [SURBL][1] and [URIBL][2]. 6 | 7 | URLs are reduced to their second level of domain (e.g. `www.example.com` is 8 | reduced to `example.com`, apart from those domains listed in 9 | `data.uribl.two_level_tlds`, which are reduced to their third level. 10 | 11 | Configuration 12 | ------------- 13 | 14 | * data.uribl.zones 15 | 16 | A list of DNS zones to query. 17 | 18 | * data.uribl.two_level_tlds 19 | 20 | A list of top level domains to extend to two levels of stripping rather 21 | than one. You may wish to add sites like wordpress.com and blogger.com 22 | to this list. 23 | 24 | [1]: http://www.surbl.org/ 25 | [2]: http://www.uribl.com/ -------------------------------------------------------------------------------- /docs/plugins/block_me.md: -------------------------------------------------------------------------------- 1 | block_me 2 | ======== 3 | 4 | This plugin allows you to configure an address which mail sent to will be 5 | parsed for a From: address in the body of the message, and will add that 6 | from address to the `mail_from.blocklist` config file. 7 | 8 | Effectively this allows your users to forward spams that got through to a 9 | particular mailbox to block them in the future. 10 | 11 | Note that this is a system-wide block, and not per-user, so you may wish to 12 | be careful about installing this. 13 | 14 | Configuration 15 | ------------- 16 | 17 | * `config/block_me.recipient` - a file containing the dropbox to email to 18 | get something blocked. For example: `spam@domain.com`. 19 | 20 | * `config/block_me.senders` - a file containing a list of email addresses 21 | that are allowed to email the dropbox. 22 | -------------------------------------------------------------------------------- /plugins/max_unrecognized_commands.js: -------------------------------------------------------------------------------- 1 | // Don't let the remote end spew us with unrecognized commands 2 | // Defaults to 10 max unrecognized commands 3 | 4 | exports.hook_connect = function(next, connection) { 5 | connection.notes.unrecognized_command_max = this.config.get('max_unrecognized_commands') || 10; 6 | connection.notes.unrecognized_command_count = 0; 7 | next(); 8 | }; 9 | 10 | exports.hook_unrecognized_command = function(next, connection, cmd) { 11 | this.loginfo("Unrecognized command: " + cmd); 12 | 13 | connection.notes.unrecognized_command_count++; 14 | if (connection.notes.unrecognized_command_count >= connection.notes.unrecognized_command_max) { 15 | this.loginfo("Closing connection. Too many bad commands."); 16 | return next(DENYDISCONNECT, "Too many bad commands"); 17 | } 18 | next(); 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Matt Sergeant (http://baudehlo.wordpress.com/)", 3 | "name": "Haraka", 4 | "description": "An SMTP Server project.", 5 | "keywords": ["haraka", "smtp", "server", "email", "cluster"], 6 | "version": "0.8.0", 7 | "homepage": "https://github.com/baudehlo/Haraka/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/baudehlo/Haraka.git" 11 | }, 12 | "main": "haraka.js", 13 | "engines": { 14 | "node": ">= v0.4.0" 15 | }, 16 | "dependencies": { 17 | "cluster": ">= 0.6.1", 18 | "nopt": ">= 1.0.5" 19 | }, 20 | "licenses": [ { 21 | "type": "MIT", 22 | "url": "https://github.com/baudehlo/Haraka/blob/master/LICENSE" 23 | } ], 24 | "bugs": { 25 | "mail" : "helpme@gmail.com", 26 | "web" : "https://github.com/baudehlo/Haraka/issues" 27 | }, 28 | "bin": { "haraka": "./bin/haraka" } 29 | } 30 | -------------------------------------------------------------------------------- /plugins/data.signatures.js: -------------------------------------------------------------------------------- 1 | // Simple string signatures 2 | 3 | exports.hook_data = function (next, connection) { 4 | // enable mail body parsing 5 | connection.transaction.parse_body = 1; 6 | next(); 7 | } 8 | 9 | exports.hook_data_post = function (next, connection) { 10 | var sigs = this.config.get('data.signatures', 'list'); 11 | 12 | if (check_sigs(sigs, connection.transaction.body)) { 13 | return next(DENY, "Mail matches a known spam signature"); 14 | } 15 | return next(); 16 | } 17 | 18 | function check_sigs (sigs, body) { 19 | for (var i=0,l=sigs.length; i < l; i++) { 20 | if (body.bodytext.indexOf(sigs[i]) != -1) { 21 | return 1; 22 | } 23 | } 24 | 25 | for (var i=0,l=body.children.length; i < l; i++) { 26 | if (check_sigs(sigs, body.children[i])) { 27 | return 1; 28 | } 29 | } 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /plugins/mail_from.is_resolvable.js: -------------------------------------------------------------------------------- 1 | // Check MAIL FROM domain is resolvable to an MX 2 | 3 | var dns = require('dns'); 4 | 5 | exports.hook_mail = function(next, connection, params) { 6 | var mail_from = params[0]; 7 | // Check for MAIL FROM without an @ first - ignore those here 8 | if (!mail_from.host) { 9 | return next(); 10 | } 11 | 12 | var domain = mail_from.host; 13 | var plugin = this; 14 | 15 | // TODO: this is too simple I think - needs work on handling DNS errors 16 | dns.resolveMx(domain, function(err, addresses) { 17 | if (err && err.code != dns.NXDOMAIN && err.code != 'ENOTFOUND') { 18 | plugin.logerror("DNS Error: " + err); 19 | return next(DENYSOFT, "Temporary resolver error"); 20 | } 21 | if (addresses && addresses.length) { 22 | return next(); 23 | } 24 | return next(DENYSOFT, "No MX for your FROM address"); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /docs/plugins/queue/smtp_forward.md: -------------------------------------------------------------------------------- 1 | queue/smtp_forward 2 | ================== 3 | 4 | This plugin delivers to another mail server. This is a common setup when you 5 | want to have a mail server with a solid pedigree of outbound delivery to 6 | other hosts, and inbound delivery to users. 7 | 8 | In comparison to `queue/smtp_proxy`, this plugin waits until queue time to 9 | attempt the ongoing connection. This can be a benefit in reducing connections 10 | to your inbound mail server when you have content filtering (such as 11 | spamassassin) enabled. However you miss out on the benefits of recipient 12 | filtering that the ongoing mail server may provide. 13 | 14 | Configuration 15 | ------------- 16 | 17 | * smtp_forward.ini 18 | 19 | Configuration is stored in this file in the following keys: 20 | 21 | * host=HOST 22 | 23 | The host to connect to. 24 | 25 | * port=PORT 26 | 27 | The port to connect to. 28 | 29 | Both values are required. 30 | -------------------------------------------------------------------------------- /tests/rfc1869.js: -------------------------------------------------------------------------------- 1 | var test = require("tap").test; 2 | var parse = require("../rfc1869").parse; 3 | var dump = require('util').inspect; 4 | 5 | var p = function (t) { 6 | var match = /^(MAIL|RCPT)\s+(.*)$/.exec(t); 7 | var out = parse(match[1].toLowerCase(), match[2]); 8 | //console.log("in: " + t + ", out: " + dump(out)); 9 | return out; 10 | } 11 | 12 | test("Basic rfc1869 tests", function (t) { 13 | t.equals(p("MAIL FROM:<>")[0], "<>"); 14 | t.equals(p("MAIL FROM:")[0], "<>"); 15 | t.equals(p("MAIL FROM:")[0], ''); 16 | t.equals(p("MAIL FROM:user")[0], 'user'); 17 | t.equals(p("MAIL FROM:user size=1234")[0], 'user'); 18 | t.equals(p("MAIL FROM:user@domain size=1234")[0], 'user@domain'); 19 | t.equals(p("MAIL FROM: size=1234")[1], 'size=1234'); 20 | t.equals(p("MAIL FROM: somekey")[1], 'somekey'); 21 | t.equals(p("MAIL FROM: somekey other=foo")[2], 'other=foo'); 22 | t.end(); 23 | }); -------------------------------------------------------------------------------- /plugins/rdns.regexp.js: -------------------------------------------------------------------------------- 1 | // check rdns against list of regexps 2 | 3 | exports.hook_connect = function (next, connection) { 4 | var deny_list = this.config.get('rdns.deny_regexps', 'list'); 5 | var allow_list = this.config.get('rdns.allow_regexps', 'list'); 6 | 7 | for (var i=0,l=deny_list.length; i < l; i++) { 8 | var re = new RegExp(deny_list[i]); 9 | if (re.test(connection.remote_host)) { 10 | for (var i=0,l=allow_list.length; i < l; i++) { 11 | var re = new RegExp(allow_list[i]); 12 | if (re.test(connection.remote_host)) { 13 | this.loginfo("rdns matched: " + allow_list[i] + 14 | ", allowing"); 15 | return next(); 16 | } 17 | } 18 | 19 | this.loginfo("rdns matched: " + deny_list[i] + ", blocking"); 20 | return next(DENY, "Connection from a known bad host"); 21 | } 22 | } 23 | 24 | return next(); 25 | }; 26 | -------------------------------------------------------------------------------- /line_socket.js: -------------------------------------------------------------------------------- 1 | // A subclass of Socket which reads data by line 2 | 3 | var net = require('net'); 4 | var util = require('util'); 5 | 6 | function Socket(options) { 7 | if (!(this instanceof Socket)) return new Socket(options); 8 | net.Socket.call(this, options); 9 | this.current_data = ''; 10 | this.on('data', this.process_data); 11 | this.on('end', this.process_end); 12 | } 13 | 14 | util.inherits(Socket, net.Socket); 15 | 16 | exports.Socket = Socket; 17 | 18 | var line_regexp = /^([^\n]*\n)/; 19 | 20 | Socket.prototype.process_data = function (data) { 21 | this.current_data += data; 22 | var results; 23 | while (results = line_regexp.exec(this.current_data)) { 24 | var this_line = results[1]; 25 | this.current_data = this.current_data.slice(this_line.length); 26 | this.emit('line', this_line); 27 | } 28 | }; 29 | 30 | Socket.prototype.process_end = function () { 31 | if (this.current_data.length) 32 | this.emit('line', this.current_data) 33 | this.current_data = ''; 34 | }; 35 | -------------------------------------------------------------------------------- /docs/plugins/rdns.regexp.md: -------------------------------------------------------------------------------- 1 | rdns.regexp 2 | =========== 3 | 4 | This plugin checks the reverse-DNS against a list of regular expressions. Any 5 | matches will result in a rejection, unless there is an allow rule to 6 | balance off broad regexes. 7 | 8 | To give an example. Assume we add a rule to deny all hosts with dynamic 9 | in the rDNS hostname (.*dynamic.*). Now we find a mail server, 10 | generaldynamics.com that is clearly a false positive. We could try 11 | to correct the original regex (clearly it is a poorly written regex), or 12 | we could add an allow rule for generaldynamics.com (.*generaldynamics\.com$). 13 | This means that even though the dynamic block rule matches, it will be 14 | superseded by the allow rule for generaldynamics.com. 15 | 16 | Configuration 17 | ------------- 18 | 19 | * rdns.deny_regexps 20 | 21 | The list of regular expressions to deny. Over broad regexes in this list 22 | can be corrected by using the allow list. 23 | 24 | * rdns.allow_regexps 25 | 26 | The list of regular expressions to allow. This list is always processed 27 | in favor of rules in the deny file. 28 | -------------------------------------------------------------------------------- /haraka.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | 5 | // this must be set before "server.js" is loaded 6 | process.env.HARAKA = process.env.HARAKA || path.resolve('.'); 7 | try { 8 | require.paths.unshift(path.join(process.env.HARAKA, 'node_modules')); 9 | } 10 | catch(e) { 11 | process.env.NODE_PATH += ':' + path.join(process.env.HARAKA, 'node_modules'); 12 | } 13 | 14 | var fs = require('fs'); 15 | var logger = require('./logger'); 16 | var server = require('./server'); 17 | 18 | exports.version = JSON.parse( 19 | fs.readFileSync(path.join(__dirname, './package.json'), 'utf8') 20 | ).version; 21 | 22 | process.on('uncaughtException', function (err) { 23 | if (err.stack) { 24 | err.stack.split("\n").forEach(logger.logcrit); 25 | } 26 | else { 27 | logger.logcrit('Caught exception: ' + err); 28 | } 29 | if (!server.ready) { 30 | logger.logcrit('Server not ready yet. Stopping.'); 31 | logger.dump_logs(); 32 | process.exit(); 33 | } 34 | }); 35 | 36 | logger.log("Starting up Haraka version " + exports.version); 37 | 38 | 39 | server.createServer(); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software takes the MIT license as described below: 2 | 3 | Copyright (C) 2011 by Matt Sergeant 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 | 23 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var configloader = require('./configfile'); 2 | var path = require('path'); 3 | var logger = require('./logger'); 4 | 5 | var config = exports; 6 | 7 | var config_path = process.env.HARAKA ? path.join(process.env.HARAKA, 'config') : path.join(__dirname, './config'); 8 | 9 | config.get = function(name, type) { 10 | if (type !== 'nolog') { 11 | logger.logdebug("Getting config: " + name); 12 | } 13 | else { 14 | type = arguments[2]; 15 | } 16 | 17 | type = type || 'value'; 18 | 19 | var full_path = path.resolve(config_path, name); 20 | 21 | var results; 22 | try { 23 | results = configloader.read_config(full_path, type); 24 | } 25 | catch (err) { 26 | if (err.code === 'EBADF' || err.code === 'ENOENT') { 27 | // no such file or directory 28 | if (type != 'value' ) { 29 | return configloader.empty_config(type); 30 | } 31 | else { 32 | return null; 33 | } 34 | } 35 | else { 36 | logger.logerror(err.name + ': ' + err.message); 37 | } 38 | } 39 | return results; 40 | }; 41 | -------------------------------------------------------------------------------- /docs/Connection.md: -------------------------------------------------------------------------------- 1 | Connection Object 2 | ================= 3 | 4 | For each connection to Haraka there is one connection object. 5 | 6 | API 7 | --- 8 | 9 | * connection.uuid 10 | 11 | A unique UUID for this connection. 12 | 13 | * connection.remote_ip 14 | 15 | The remote IP address 16 | 17 | * connection.remote_host 18 | 19 | The rDNS of the remote IP 20 | 21 | * connection.greeting 22 | 23 | Either 'EHLO' or 'HELO' whichever the remote end used 24 | 25 | * connection.hello_host 26 | 27 | The hostname given to HELO or EHLO 28 | 29 | * connection.notes 30 | 31 | A safe object in which you can store connection-specific variables 32 | 33 | * connection.transaction 34 | 35 | The current transaction object, valid after MAIL FROM, and destroyed at queue 36 | time, RSET time, or if MAIL FROM was rejected. See the Transaction Object 37 | documentation file. 38 | 39 | * connection.relaying 40 | 41 | A boolean flag to say whether this connection is allowed to relay mails (i.e. 42 | deliver mails outbound). This is normally set by SMTP AUTH, or sometimes via 43 | an IP address check. 44 | 45 | * connection.current_line 46 | 47 | For low level use. Contains the current line sent from the remote end, 48 | verbatim as it was sent. Can be useful in certain botnet detection techniques. 49 | -------------------------------------------------------------------------------- /docs/plugins/helo.checks.md: -------------------------------------------------------------------------------- 1 | helo.checks 2 | =========== 3 | 4 | This plugin performs a number of checks on the HELO string. 5 | 6 | HELO strings are very often forged or dubious in spam and so this can be a 7 | highly effective and false-positive free anti-spam measure. 8 | 9 | Configuration 10 | ------------- 11 | 12 | * helo.checks.regexps 13 | 14 | List of regular expressions to match against the HELO string. The regular 15 | expressions are automatically wrapped in `^` and `$` so they always match 16 | the entire string. 17 | 18 | * helo.checks.ini 19 | 20 | INI file which controls enabling of certain checks: 21 | 22 | * check_no_dot=1 23 | 24 | Checks that the HELO has at least one '.' in it. 25 | 26 | * check_raw_ip=1 27 | 28 | Checks for HELO where the IP is not surrounded by square brackets. 29 | This is an RFC violation so should always be enabled. 30 | 31 | * [bigco] 32 | 33 | A list of =[,...] to match against. If the HELO matches 34 | what's on the left hand side, the reverse-DNS must match one of the 35 | entries on the right hand side or the mail is blocked. 36 | 37 | Example: 38 | 39 | yahoo.com=yahoo.com,yahoo.co.jp 40 | aol.com=aol.com 41 | -------------------------------------------------------------------------------- /docs/plugins/queue/smtp_proxy.md: -------------------------------------------------------------------------------- 1 | queue/smtp_proxy 2 | ================ 3 | 4 | This plugin delivers to another mail server. This is a common setup when you 5 | want to have a mail server with a solid pedigree of outbound delivery to 6 | other hosts, and inbound delivery to users. 7 | 8 | In comparison to `queue/smtp_forward`, this plugin makes a connection at 9 | MAIL FROM time to the ongoing SMTP server. This can be a benefit in that 10 | you get any SMTP-time filtering that the ongoing server provides, in 11 | particular one important facility to some setups is recipient filtering. 12 | However be aware that other than connect and HELO-time filtering, you will 13 | have as many connections to your ongoing SMTP server as you have to Haraka. 14 | 15 | Configuration 16 | ------------- 17 | 18 | * smtp_proxy.ini 19 | 20 | Configuration is stored in this file in the following keys: 21 | 22 | * host=HOST 23 | 24 | The host to connect to. 25 | 26 | * port=PORT 27 | 28 | The port to connect to. 29 | 30 | * timeout=SECONDS 31 | 32 | The amount of seconds to let a backend connection live idle in the 33 | proxy pool. This should always be less than the global plugin timeout, 34 | which should in turn be less than the connection timeout. 35 | 36 | Both values are required. 37 | 38 | -------------------------------------------------------------------------------- /plugins/rcpt_to.in_host_list.js: -------------------------------------------------------------------------------- 1 | // Check RCPT TO domain is in host list 2 | 3 | exports.hook_rcpt = function(next, connection, params) { 4 | var rcpt = params[0]; 5 | // Check for RCPT TO without an @ first - ignore those here 6 | if (!rcpt.host) { 7 | return next(); 8 | } 9 | 10 | this.loginfo("Checking if " + rcpt + " host is in host_lists"); 11 | 12 | var domain = rcpt.host.toLowerCase(); 13 | var host_list = this.config.get('host_list', 'list'); 14 | var host_list_regex = this.config.get('host_list_regex', 'list'); 15 | 16 | var i = 0; 17 | for (i in host_list) { 18 | this.logdebug("checking " + domain + " against " + host_list[i]); 19 | 20 | // normal matches 21 | if (host_list[i].toLowerCase() === domain) { 22 | this.logdebug("Allowing " + domain); 23 | return next(OK); 24 | } 25 | } 26 | 27 | for (i in host_list_regex) { 28 | this.logdebug("checking " + domain + " against regexp " + 29 | host_list_regex[i]); 30 | 31 | var regex = new RegExp ('^' + host_list_regex[i] + '$', 'i'); 32 | 33 | // regex matches 34 | if (domain.match(regex)) { 35 | this.logdebug("Allowing " + domain); 36 | return next(OK); 37 | } 38 | } 39 | 40 | next(); 41 | } 42 | -------------------------------------------------------------------------------- /docs/Header.md: -------------------------------------------------------------------------------- 1 | Header Object 2 | ============= 3 | 4 | The Header object gives programmatic access to email headers. It is primarily 5 | used from `transaction.header` but also each MIME part of the `Body` will 6 | also have its own header object. 7 | 8 | API 9 | --- 10 | 11 | * header.get(key) 12 | 13 | Returns the header with the name `key`. If there are multiple headers with 14 | the given name (as is usually the case with "Received" for example) they will 15 | be concatenated together with "\n". 16 | 17 | * header.get_all(key) 18 | 19 | Returns the headers with the name `key` as an array. Multi-valued headers 20 | will have multiple entries in the array. 21 | 22 | * header.get_decoded(key) 23 | 24 | Works like `get(key)`, only it gives you headers decoded from any MIME encoding 25 | they may have used. 26 | 27 | * header.remove(key) 28 | 29 | Removes all headers with the given name. DO NOT USE. This is transparent to 30 | the transaction and it will not see the header(s) you removed. Instead use 31 | `transaction.remove_header(key)` which will also correct the data part of 32 | the email. 33 | 34 | * header.add(key, value) 35 | 36 | Adds a header with the given name and value. DO NOT USE. This is transparent 37 | to the transaction and it will not see the header you added. Instead use 38 | `transaction.add_header(key, value)` which will add the header to the data 39 | part of the email. 40 | 41 | * header.lines() 42 | 43 | Returns the entire header as a list of lines. 44 | 45 | * header.toString() 46 | 47 | Returns the entire header as a string. 48 | -------------------------------------------------------------------------------- /docs/Address.md: -------------------------------------------------------------------------------- 1 | Address Object 2 | ============== 3 | 4 | The Address object is an interface to reading email addresses passed in at 5 | SMTP time. As such it parses all the formats in RFC-2821 and 2822, and 6 | supports correctly escaping email addresses. 7 | 8 | API 9 | --- 10 | 11 | * new Address (user, host) 12 | 13 | Create a new address object for user@host 14 | 15 | * new Address (email) 16 | 17 | Creates a new address object by parsing the email address. Will throw an 18 | exception if the address cannot be parsed. 19 | 20 | * address.user 21 | 22 | Access the local part of the email address 23 | 24 | * address.host 25 | 26 | Access the domain part of the email adress 27 | 28 | * address.format() 29 | 30 | Provides the email address in the appropriate `` format. And 31 | deals correctly with the null sender and local names. 32 | 33 | * address.toString() 34 | 35 | Same as format(). 36 | 37 | * address.address() 38 | 39 | Provides the email address in 'user@host' format. 40 | 41 | Advanced Usage 42 | -------------- 43 | 44 | It is possible to mess with the regular expressions used to match addresses 45 | for stricter or less strict matching. 46 | 47 | To change the behaviour mess with the following variables: 48 | 49 | var adr = require('./address'); 50 | // Now change one of the following. Note they are RegExp objects NOT strings. 51 | adr.atom_expr; 52 | adr.address_literal_expr; 53 | adr.subdomain_expr; 54 | adr.domain_expr; 55 | adr.qtext_expr; 56 | adr.text_expr; 57 | // Don't forget to recompile: 58 | adr.compile_re(); 59 | -------------------------------------------------------------------------------- /tests/address.js: -------------------------------------------------------------------------------- 1 | var test = require("tap").test; 2 | require('../configfile').watch_files = false; 3 | var Address = require("../address").Address; 4 | 5 | var addresses = [ 6 | '<>', {user: null, host: null}, 7 | '', {user: 'postmaster', host: null}, 8 | '', {user: 'foo', host: 'example.com'}, 9 | '<"musa_ibrah@caramail.comandrea.luger"@wifo.ac.at>', {user: 'musa_ibrah@caramail.comandrea.luger', host: 'wifo.ac.at'}, 10 | '', {user: 'foo bar', host: 'example.com'}, 11 | 'foo@example.com', {user: 'foo', host: 'example.com'}, 12 | '', {user: 'foo', host: 'foo.x.example.com'}, 13 | 'foo@foo.x.example.com', {user: 'foo', host: 'foo.x.example.com'}, 14 | ]; 15 | 16 | test("Email Address Parsing", function(t) { 17 | // t.plan(addresses.length); 18 | for (var i=1,l=addresses.length; i', 29 | ] 30 | 31 | test("Addresses that fail", function (t) { 32 | t.plan(bad_addresses.length); 33 | for (var i=0; i < bad_addresses.length; i++) { 34 | try { 35 | var a = new Address(bad_addresses[i]); 36 | // shouldn't get here... 37 | t.ok(false, "Parse worked? " + bad_addresses[i]) 38 | } 39 | catch (e) { 40 | t.ok(1, "Exception occurred for: " + bad_addresses[i]); 41 | } 42 | } 43 | t.end(); 44 | }) -------------------------------------------------------------------------------- /plugins/tls.js: -------------------------------------------------------------------------------- 1 | // Enables TLS. This is built into the server anyway, but enabling this plugin 2 | // just advertises it. 3 | 4 | var utils = require('./utils'); 5 | 6 | // To create a key: 7 | // openssl req -x509 -nodes -days 2190 -newkey rsa:1024 \ 8 | // -keyout config/tls_key.pem -out config/tls_cert.pem 9 | 10 | exports.hook_capabilities = function (next, connection) { 11 | /* Caution: We cannot advertise STARTTLS if the upgrade has already been done. */ 12 | if (connection.notes.tls_enabled !== 1) { 13 | connection.capabilities.push('STARTTLS'); 14 | connection.notes.tls_enabled = 1; 15 | } 16 | /* Let the plugin chain continue. */ 17 | next(); 18 | }; 19 | 20 | exports.hook_unrecognized_command = function (next, connection, params) { 21 | /* Watch for STARTTLS directive from client. */ 22 | if (params[0] === 'STARTTLS') { 23 | var key = this.config.get('tls_key.pem', 'list').join("\n"); 24 | var cert = this.config.get('tls_cert.pem', 'list').join("\n"); 25 | var options = { key: key, cert: cert }; 26 | 27 | /* Respond to STARTTLS command. */ 28 | connection.respond(220, "Go ahead."); 29 | /* Upgrade the connection to TLS. */ 30 | connection.client.upgrade(options); // Use the options which were saved by starttls.createServer(). 31 | /* Force the startup protocol to repeat. */ 32 | connection.uuid = utils.uuid(); 33 | connection.reset_transaction(); 34 | connection.hello_host = undefined; 35 | connection.using_tls = true; 36 | /* Return OK since we responded to the client. */ 37 | return next(OK); 38 | } 39 | /* Let the plugin chain continue. */ 40 | next(); 41 | }; 42 | -------------------------------------------------------------------------------- /plugins/dnsbl.js: -------------------------------------------------------------------------------- 1 | // dnsbl plugin 2 | 3 | var dns = require('dns'); 4 | 5 | exports.register = function() { 6 | this.zones = this.config.get('dnsbl.zones', 'list'); 7 | this.register_hook('connect', 'check_ip'); 8 | } 9 | 10 | exports.check_ip = function(next, connection) { 11 | this.logdebug("check_ip: " + connection.remote_ip); 12 | 13 | var ip = new String(connection.remote_ip); 14 | var reverse_ip = ip.split('.').reverse().join('.'); 15 | 16 | if (!this.zones || !this.zones.length) { 17 | this.logerror("No zones"); 18 | return next(); 19 | } 20 | 21 | var remaining_zones = []; 22 | 23 | var self = this; 24 | this.zones.forEach(function(zone) { 25 | self.logdebug("Querying: " + reverse_ip + "." + zone); 26 | dns.resolve(reverse_ip + "." + zone, "TXT", function (err, value) { 27 | if (!remaining_zones.length) return; 28 | remaining_zones.pop(); // we don't care about order really 29 | if (err) { 30 | switch (err.code) { 31 | case dns.NOTFOUND: 32 | case dns.NXDOMAIN: 33 | case 'ENOTFOUND': 34 | break; 35 | default: 36 | self.loginfo("DNS error: " + err); 37 | } 38 | if (remaining_zones.length === 0) { 39 | // only call declined if no more results are pending 40 | return next(); 41 | } 42 | return; 43 | } 44 | remaining_zones = []; 45 | return next(DENY, value); 46 | }); 47 | 48 | remaining_zones.push(zone); 49 | }); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | // Various utility functions 2 | 3 | // copied from http://www.broofa.com/Tools/Math.uuid.js 4 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 5 | 6 | exports.uuid = function () { 7 | var chars = CHARS, uuid = new Array(36), rnd=0, r; 8 | for (var i = 0; i < 36; i++) { 9 | if (i==8 || i==13 || i==18 || i==23) { 10 | uuid[i] = '-'; 11 | } 12 | else if (i==14) { 13 | uuid[i] = '4'; 14 | } 15 | else { 16 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; 17 | r = rnd & 0xf; 18 | rnd = rnd >> 4; 19 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 20 | } 21 | } 22 | return uuid.join(''); 23 | }; 24 | 25 | exports.in_array = function (item, array) { 26 | for (var i in array) { 27 | if (item === array[i]) { 28 | return true; 29 | } 30 | } 31 | return false; 32 | }; 33 | 34 | exports.sort_keys = function (obj) { 35 | return Object.keys(obj).sort(); 36 | }; 37 | 38 | exports.uniq = function (arr) { 39 | var out = []; 40 | var o = 0; 41 | for (var i=0,l=arr.length; i < l; i++) { 42 | if (out.length === 0) { 43 | out.push(arr[i]); 44 | } 45 | else if (out[o] != arr[i]) { 46 | out.push(arr[i]); 47 | o++; 48 | } 49 | } 50 | return out; 51 | } 52 | 53 | exports.ISODate = function (d) { 54 | function pad(n) {return n<10 ? '0'+n : n} 55 | return d.getUTCFullYear()+'-' 56 | + pad(d.getUTCMonth()+1)+'-' 57 | + pad(d.getUTCDate())+'T' 58 | + pad(d.getUTCHours())+':' 59 | + pad(d.getUTCMinutes())+':' 60 | + pad(d.getUTCSeconds())+'Z' 61 | } 62 | -------------------------------------------------------------------------------- /docs/Transaction.md: -------------------------------------------------------------------------------- 1 | Transaction Object 2 | ================== 3 | 4 | An SMTP transaction is valid from MAIL FROM time until RSET or "final-dot". 5 | 6 | API 7 | --- 8 | 9 | * transaction.uuid 10 | 11 | A unique UUID for this transaction. Is equal to the connection.uuid + '.N' 12 | where N increments for each transaction on this connection. 13 | 14 | * transaction.mail\_from 15 | 16 | The value of the MAIL FROM command as an `Address` object. 17 | 18 | * transaction.rcpt\_to 19 | 20 | An Array of `Address` objects of recipients from the RCPT TO command. 21 | 22 | * transaction.data\_lines 23 | 24 | An Array of the lines of the email after DATA. 25 | 26 | * transaction.data\_bytes 27 | 28 | The number of bytes in the email after DATA. 29 | 30 | * transaction.add_data(line) 31 | 32 | Adds a line of data to the email. Note this is RAW email - it isn't useful 33 | for adding banners to the email. 34 | 35 | * transaction.notes 36 | 37 | A safe place to store transaction specific values. 38 | 39 | * transaction.add_header(key, value) 40 | 41 | Adds a header to the email. 42 | 43 | * transaction.remove_header(key) 44 | 45 | Deletes a header from the email. 46 | 47 | * transaction.header 48 | 49 | The header of the email. See `Header Object`. 50 | 51 | * transaction.parse_body 52 | 53 | Set to 1 to enable parsing of the mail body. Make sure you set this in 54 | hook_data or before. 55 | 56 | * transaction.body 57 | 58 | The body of the email if you set `parse_body` above. See `Body Object`. 59 | 60 | * transaction.attachment_hooks(start, data, end) 61 | 62 | Sets event emitter hooks for attachments if you set `parse_body` above. 63 | 64 | The `start` event will receive `(content_type, filename, body)` as parameters. 65 | 66 | The `data` event will receive a `Buffer` object containing some of the 67 | attachment data. 68 | 69 | The `end` event will be called with no parameters when an attachment ends. 70 | 71 | Both the `data` and `end` params are optional. 72 | 73 | Note that in the `start` event, you can set per-attachment events via: 74 | 75 | body.on('attachment_data', cb) 76 | body.on('attachment_end', cb) 77 | -------------------------------------------------------------------------------- /transaction.js: -------------------------------------------------------------------------------- 1 | // An SMTP Transaction 2 | 3 | var config = require('./config'); 4 | var logger = require('./logger'); 5 | var Header = require('./mailheader').Header; 6 | var body = require('./mailbody'); 7 | var utils = require('./utils'); 8 | 9 | var trans = exports; 10 | 11 | function Transaction() { 12 | this.mail_from = null; 13 | this.rcpt_to = []; 14 | this.data_lines = []; 15 | this.data_bytes = 0; 16 | this.header_pos = 0; 17 | this.parse_body = false; 18 | this.notes = {}; 19 | this.header = new Header(); 20 | } 21 | 22 | exports.Transaction = Transaction; 23 | 24 | exports.createTransaction = function(uuid) { 25 | var t = new Transaction(); 26 | t.uuid = uuid || utils.uuid(); 27 | return t; 28 | }; 29 | 30 | Transaction.prototype.add_data = function(line) { 31 | this.data_bytes += line.length; 32 | // check if this is the end of headers line (note the regexp isn't as strong 33 | // as it should be - it accepts whitespace in a blank line - we've found this 34 | // to be a good heuristic rule though). 35 | if (this.header_pos === 0 && line.match(/^\s*$/)) { 36 | this.header.parse(this.data_lines); 37 | this.header_pos = this.data_lines.length; 38 | if (this.parse_body) { 39 | this.body = this.body || new body.Body(this.header); 40 | } 41 | } 42 | else if (this.header_pos && this.parse_body) { 43 | this.body.parse_more(line); 44 | } 45 | this.data_lines.push(line); 46 | }; 47 | 48 | Transaction.prototype.add_header = function(key, value) { 49 | this.header.add(key, value); 50 | this.reset_headers(); 51 | }; 52 | 53 | Transaction.prototype.reset_headers = function () { 54 | var header_lines = this.header.lines(); 55 | this.data_lines = header_lines.concat(this.data_lines.slice(this.header_pos)); 56 | this.header_pos = header_lines.length; 57 | }; 58 | 59 | Transaction.prototype.remove_header = function (key) { 60 | this.header.remove(key); 61 | this.reset_headers(); 62 | }; 63 | 64 | Transaction.prototype.attachment_hooks = function (start, data, end) { 65 | this.parse_body = 1; 66 | this.body = this.body || new body.Body(this.header); 67 | this.body.on('attachment_start', start); 68 | if (data) 69 | this.body.on('attachment_data', data); 70 | if (end) 71 | this.body.on('attachment_end', end); 72 | }; 73 | -------------------------------------------------------------------------------- /docs/Config.md: -------------------------------------------------------------------------------- 1 | Config Files 2 | ============ 3 | 4 | Haraka contains a flexible config loader which can load a few different types 5 | of configuration files. 6 | 7 | The API is fairly simple: 8 | 9 | // From within a plugin: 10 | var config_item = this.config.get(name, [type='value']); 11 | 12 | Where type can be one of: 13 | 14 | * 'ini' - load an "ini" style file 15 | * 'value' - load a flat file containing a single value (default) 16 | * 'list' - load a flat file containing a list of values 17 | 18 | The name is not a filename, but a name in the config/ directory. For example: 19 | 20 | var config_item = this.config.get('rambling.paths', 'list'); 21 | 22 | This will look up and load the file config/rambling.paths in the Haraka 23 | directory. 24 | 25 | File Formats 26 | ============ 27 | 28 | Ini Files 29 | --------- 30 | 31 | INI files had their heritage in early versions of Microsoft Windows products. 32 | 33 | They are a simple format of key=value pairs, with an optional [section]. 34 | 35 | Here is a typical example: 36 | 37 | first_name=Matt 38 | last_name=Sergeant 39 | 40 | [job] 41 | title=Senior Principal Software Engineer 42 | role=Architect 43 | 44 | That produces the following Javascript object: 45 | 46 | { 47 | main: { 48 | first_name: 'Matt', 49 | last_name: 'Sergeant' 50 | }, 51 | job: { 52 | title: 'Senior Principal Software Engineer', 53 | role: 'Architect' 54 | } 55 | } 56 | 57 | The key point there is that items before any [section] marker go in the "main" 58 | section. 59 | 60 | Flat Files 61 | ---------- 62 | 63 | Flat files are simply either lists of values separated by \n or a single 64 | value in a file on its own. Those who have used qmail or qpsmtpd will be 65 | familiar with this format. 66 | 67 | Lines starting with '#' and blank lines will be ignored. 68 | 69 | See plugins/dnsbl.js for an example. 70 | 71 | Reloading/Caching 72 | ======== 73 | 74 | Haraka automatically reloads configuration files, but this will only help if 75 | whatever is looking at that config re-calls config.get() to re-access the 76 | configuration file after it has changed. Configuration files are watched for 77 | changes so this process is not a heavyweight "poll" process, and files are 78 | not re-read every time config.get() is called so this can be considered a 79 | lightweight process. 80 | -------------------------------------------------------------------------------- /plugins/helo.checks.js: -------------------------------------------------------------------------------- 1 | // Check various bits of the HELO string 2 | 3 | // Checks to implement: 4 | // - HELO has no "dot" 5 | // - List of regexps 6 | // - HELO raw IP 7 | // - HELO looks dynamic 8 | // - Well known HELOs that must match rdns 9 | 10 | exports.register = function () { 11 | var plugin = this; 12 | ['helo_no_dot', 13 | 'helo_match_re', 14 | 'helo_raw_ip', 15 | 'helo_is_dynamic', 16 | 'helo_big_company' 17 | ].forEach(function (hook) { 18 | plugin.register_hook('helo', hook); 19 | plugin.register_hook('ehlo', hook); 20 | }); 21 | } 22 | 23 | exports.helo_no_dot = function (next, connection, helo) { 24 | var config = this.config.get('helo.checks.ini', 'ini'); 25 | if (!config.main.check_no_dot) { 26 | return next(); 27 | } 28 | 29 | /\./.test(helo) ? next() : next(DENY, "HELO must have a dot"); 30 | }; 31 | 32 | exports.helo_match_re = function (next, connection, helo) { 33 | var regexps = this.config.get('helo.checks.regexps', 'list'); 34 | 35 | for (var i=0,l=regexps.length; i < l; i++) { 36 | var re = new RegExp('^' + regexps[i] + '$'); 37 | if (re.test(helo)) { 38 | return next(DENY, "BAD HELO"); 39 | } 40 | } 41 | return next(); 42 | }; 43 | 44 | exports.helo_raw_ip = function (next, connection, helo) { 45 | var config = this.config.get('helo.checks.ini', 'ini'); 46 | if (!config.main.check_raw_ip) { 47 | return next(); 48 | } 49 | 50 | // RAW IPs must be formatted: "[1.2.3.4]" not "1.2.3.4" in HELOs 51 | /^\d+\.\d+\.\d+\.\d+$/.test(helo) ? 52 | next(DENY, "RAW IP HELOs must be correctly formatted") 53 | : next(); 54 | }; 55 | 56 | exports.helo_is_dynamic = function (next, connection, helo) { 57 | return next(); // TODO! 58 | }; 59 | 60 | exports.helo_big_company = function (next, connection, helo) { 61 | var rdns = connection.remote_host; 62 | 63 | var big_co = this.config.get('helo.checks.ini', 'ini').bigco; 64 | if (big_co[helo]) { 65 | var allowed_rdns = big_co[helo].split(/,/); 66 | for (var i=0,l=allowed_rdns.length; i < l; i++) { 67 | var re = new RegExp(allowed_rdns[i].replace(/\./g, '\\.') + '$'); 68 | if (re.test(rdns)) { 69 | return next(); 70 | } 71 | } 72 | return next(DENY, "You are not who you say you are"); 73 | } 74 | else { 75 | return next(); 76 | } 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /plugins/auth/flat_file.js: -------------------------------------------------------------------------------- 1 | // Auth against a flat file 2 | 3 | var crypto = require('crypto'); 4 | 5 | exports.hook_capabilities = function (next, connection) { 6 | connection.capabilities.push('AUTH CRAM-MD5'); 7 | next(); 8 | } 9 | 10 | exports.hook_unrecognized_command = function (next, connection, params) { 11 | if (connection.notes.auth_flat_file_ticket) { 12 | var credentials = unbase64(params[0]).split(' '); 13 | return this.check_user(next, connection, credentials); 14 | } 15 | else if (params[0] === 'AUTH' && params[1] === 'CRAM-MD5') { 16 | var ticket = '<' + hexi(Math.floor(Math.random() * 1000000)) + '.' + 17 | hexi(Date.now()) + '@' + this.config.get('me') + '>'; 18 | this.loginfo("ticket: " + ticket); 19 | connection.respond(334, base64(ticket)); 20 | connection.notes.auth_flat_file_ticket = ticket; 21 | return next(OK); 22 | } 23 | return next(); 24 | } 25 | 26 | exports.check_user = function (next, connection, credentials) { 27 | if (!(credentials[0] && credentials[1])) { 28 | connection.respond(504, "Invalid AUTH string"); 29 | connection.reset_transaction(); 30 | return next(OK); 31 | } 32 | 33 | var config = this.config.get('auth_flat_file.ini', 'ini'); 34 | 35 | if (!config.users[credentials[0]]) { 36 | connection.respond(535, "Authentication failed for " + credentials[0]); 37 | connection.reset_transaction(); 38 | return next(OK); 39 | } 40 | 41 | var clear_pw = config.users[credentials[0]]; 42 | 43 | var hmac = crypto.createHmac('md5', clear_pw); 44 | hmac.update(connection.notes.auth_flat_file_ticket); 45 | var hmac_pw = hmac.digest('hex'); 46 | 47 | this.loginfo("comparing " + hmac_pw + ' to ' + credentials[1]); 48 | 49 | if (hmac_pw === credentials[1]) { 50 | connection.relaying = 1; 51 | connection.respond(235, "Authentication successful"); 52 | } 53 | else { 54 | connection.respond(535, "Authentication failed"); 55 | connection.reset_transaction(); 56 | } 57 | return next(OK); 58 | } 59 | 60 | function hexi (number) { 61 | return String(Math.abs(parseInt(number)).toString(16)); 62 | } 63 | 64 | function base64 (str) { 65 | var buffer = new Buffer(str, "UTF-8"); 66 | return buffer.toString("base64"); 67 | } 68 | 69 | function unbase64 (str) { 70 | var buffer = new Buffer(str, "base64"); 71 | return buffer.toString("UTF-8"); 72 | } -------------------------------------------------------------------------------- /plugins/queue/qmail-queue.js: -------------------------------------------------------------------------------- 1 | // Queue to qmail-queue 2 | 3 | var childproc = require('child_process'); 4 | var net = require('net'); 5 | var netBinding = process.binding('net'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | exports.register = function () { 10 | this.queue_exec = this.config.get('qmail-queue.path') || '/var/qmail/bin/qmail-queue'; 11 | if (!path.existsSync(this.queue_exec)) { 12 | throw new Error("Cannot find qmail-queue binary (" + this.queue_exec + ")"); 13 | } 14 | }; 15 | 16 | exports.hook_queue = function (next, connection) { 17 | var plugin = this; 18 | var messagePipe = netBinding.pipe(); 19 | var envelopePipe = netBinding.pipe(); 20 | var qmail_queue = childproc.spawn( 21 | this.queue_exec, // process name 22 | [], // arguments 23 | { customFds: [messagePipe[0], envelopePipe[0]] } 24 | ); 25 | 26 | var finished = function (code) { 27 | fs.close(messagePipe[0]); 28 | fs.close(envelopePipe[0]); 29 | if (code !== 0) { 30 | plugin.logerror("Unable to queue message to qmail-queue: " + code); 31 | next(); 32 | } 33 | else { 34 | next(OK, "Queued!"); 35 | } 36 | }; 37 | 38 | qmail_queue.on('exit', finished); 39 | 40 | var i = 0; 41 | var write_more = function () { 42 | if (i === connection.transaction.data_lines.length) { 43 | return fs.close(messagePipe[1], function () { 44 | // now send envelope 45 | // Hope this will be big enough... 46 | var buf = new Buffer(4096); 47 | var p = 0; 48 | buf[p++] = 70; 49 | var mail_from = connection.transaction.mail_from.address(); 50 | for (var i = 0; i < mail_from.length; i++) { 51 | buf[p++] = mail_from.charCodeAt(i); 52 | } 53 | buf[p++] = 0; 54 | connection.transaction.rcpt_to.forEach(function (rcpt) { 55 | buf[p++] = 84; 56 | var rcpt_to = rcpt.address(); 57 | for (var i = 0; i < rcpt_to.length; i++) { 58 | buf[p++] = rcpt_to.charCodeAt(i); 59 | } 60 | buf[p++] = 0; 61 | }); 62 | buf[p++] = 0; 63 | fs.write(envelopePipe[1], buf, 0, p, null, function () { 64 | fs.close(envelopePipe[1]); 65 | // now we just wait for the process to exit, which happens above 66 | }); 67 | }); 68 | } 69 | var buf = new Buffer(connection.transaction.data_lines[i]); 70 | i++; 71 | fs.write(messagePipe[1], buf, 0, buf.length, null, write_more); 72 | }; 73 | 74 | write_more(); 75 | }; 76 | -------------------------------------------------------------------------------- /plugins/block_me.js: -------------------------------------------------------------------------------- 1 | // Plugin which registers mail received to a certain address 2 | // and extracts a From: address from the mail and puts that address 3 | // in the mail_from.blocklist file. You need to be running the 4 | // mail_from.blocklist plugin for this to work fully. 5 | 6 | var fs = require('fs'); 7 | var utils = require('./utils'); 8 | 9 | exports.hook_data = function (next, connection) { 10 | // enable mail body parsing 11 | connection.transaction.parse_body = 1; 12 | next(); 13 | } 14 | 15 | exports.hook_data_post = function (next, connection) { 16 | if (!connection.relaying) { 17 | return next(); 18 | } 19 | 20 | var recip = (this.config.get('block_me.recipient') || '').toLowerCase(); 21 | var senders = this.config.get('block_me.senders', 'list'); 22 | 23 | // Make sure only 1 recipient 24 | if (connection.transaction.rcpt_to.length != 1) { 25 | return next(); 26 | } 27 | 28 | // Check recipient is the right one 29 | if (connection.transaction.rcpt_to[0].address().toLowerCase() != recip) { 30 | return next(); 31 | } 32 | 33 | // Check sender is in list 34 | var sender = connection.transaction.mail_from.address(); 35 | if (!utils.in_array(sender, senders)) { 36 | return next(DENY, "You are not allowed to block mail, " + sender); 37 | } 38 | 39 | // Now extract the "From" from the body... 40 | var to_block = extract_from_line(connection.transaction.body); 41 | if (!to_block) { 42 | this.logerror("No sender found in email"); 43 | return next(); 44 | } 45 | 46 | this.loginfo("Blocking new sender: " + to_block); 47 | 48 | connection.transaction.notes.block_me = 1; 49 | 50 | // add to mail_from.blocklist 51 | fs.open('./config/mail_from.blocklist', 'a', function (err, fd) { 52 | if (!err) { 53 | fs.write(fd, to_block + "\n", null, 'UTF-8', function (err, written) { 54 | fs.close(fd); 55 | }); 56 | } 57 | else { 58 | plugin.logerror("Unable to append to mail_from.blocklist: " + err); 59 | } 60 | }); 61 | 62 | next(); 63 | } 64 | 65 | exports.hook_queue = function (next, connection) { 66 | if (connection.transaction.notes.block_me) { 67 | // pretend we queued this mail 68 | return next(OK); 69 | } 70 | 71 | next(); 72 | } 73 | 74 | // Example: From: Site Tucano Gold 75 | function extract_from_line(body) { 76 | var matches = body.bodytext.match(/\bFrom:[^<\n]*<([^>\n]*)>/); 77 | if (matches) { 78 | return matches[1]; 79 | } 80 | 81 | for (var i=0,l=body.children.length; i < l; i++) { 82 | var from = extract_from_line(body.children[i]); 83 | if (from) { 84 | return from; 85 | } 86 | } 87 | 88 | return null; 89 | } 90 | -------------------------------------------------------------------------------- /docs/plugins/lookup_rdns.strict.md: -------------------------------------------------------------------------------- 1 | lookup_rdns.strict 2 | =========== 3 | 4 | This plugin checks the reverse-DNS and compares the resulting addresses 5 | against forward DNS for a match. If there is no match it sends a 6 | DENYDISCONNECT, otherwise if it matches it sends an OK. DENYDISCONNECT 7 | messages are configurable. 8 | 9 | Configuration lookup_rdns.strict.ini 10 | -------------------------------------------- 11 | 12 | This is the general configuration file for the plugin. In it you can find 13 | ways to customize user messages, specify timeouts, and some whitelist 14 | parsing options. 15 | 16 | * lookup_rdns.strict.general.nomatch 17 | 18 | Text to send the user if there is no reverse to forward match (text). 19 | 20 | 21 | * lookup_rdns.strict.general.timeout 22 | 23 | How long we should give this plugin before we time it out (seconds). 24 | 25 | 26 | * lookup_rdns.strict.general.timeout_msg 27 | 28 | Text to send when plugin reaches timeout (text). 29 | 30 | 31 | * lookup_rdns.strict.forward.nxdomain 32 | 33 | Text to send the user if there is no forward match (text). 34 | 35 | 36 | * lookup_rdns.strict.forward.dnserror 37 | 38 | Text to send the user if there is some other error with the forward 39 | lookup (text). 40 | 41 | 42 | * lookup_rdns.strict.reverse.nxdomain 43 | 44 | Text to send the user if there is no reverse match (text). 45 | 46 | 47 | * lookup_rdns.strict.reverse.dnserror 48 | 49 | Text to send the user if there is some other error with the reverse 50 | lookup (text). 51 | 52 | 53 | Configuration lookup_rdns.strict.timeout 54 | ------------------------------------------------ 55 | 56 | This is how we specify to Haraka that our plugin should have a certain timeout. 57 | If you specify 0 here, then the plugin will never timeout while the connection 58 | is active. This is also required for this plugin, which needs to handle its 59 | own timeouts. To actually specify the timeout for this plugin, please see 60 | the general config in lookup_rdns.strict.ini. 61 | 62 | Configuration lookup_rdns.strict.whitelist 63 | -------------------------------------------------- 64 | 65 | No matter how much you believe in checking that DNS and rDNS match, it is not 66 | required by RFC, and there will always be some legitimate mail server that 67 | has great trouble getting their DNS in order. For this reason we are 68 | providing a whitelist. 69 | 70 | This file will match exactly what you put on each line. 71 | 72 | 73 | Configuration lookup_rdns.strict.whitelist_regex 74 | -------------------------------------------------------- 75 | 76 | Does the same thing as the whitelist file, but each line is a regex. 77 | Each line is also anchored for you, meaning '^' + regex + '$' is added for 78 | you. If you need to get around this restriction, you may use a '.*' at 79 | either the start or the end of your regex. This should help prevent people 80 | from writing overly permissive rules on accident. 81 | -------------------------------------------------------------------------------- /rfc1869.js: -------------------------------------------------------------------------------- 1 | // RFC 1869 command parser 2 | 3 | // 6. MAIL FROM and RCPT TO Parameters 4 | // [...] 5 | // 6 | // esmtp-cmd ::= inner-esmtp-cmd [SP esmtp-parameters] CR LF 7 | // esmtp-parameters ::= esmtp-parameter *(SP esmtp-parameter) 8 | // esmtp-parameter ::= esmtp-keyword ["=" esmtp-value] 9 | // esmtp-keyword ::= (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") 10 | // 11 | // ; syntax and values depend on esmtp-keyword 12 | // esmtp-value ::= 1* like 48 | // MAIL FROM: user=name@example.net 49 | // or RCPT TO: postmaster 50 | 51 | // let's see if $line contains nothing and use the first value as address: 52 | if (line.length) { 53 | // parameter syntax error, i.e. not all of the arguments were 54 | // stripped by the while() loop: 55 | if (line.match(/\@.*\s/)) { 56 | throw new Error("Syntax error in parameters (" + line + ")"); 57 | } 58 | 59 | params.unshift(line); 60 | 61 | return params; 62 | } 63 | 64 | line = params.shift() || ''; 65 | if (type === "mail") { 66 | if (!line.length) { 67 | return ["<>"]; // 'MAIL FROM:' --> 'MAIL FROM:<>' 68 | } 69 | if (line.match(/\@.*\s/)) { 70 | throw new Error("Syntax error in parameters"); 71 | } 72 | } 73 | else { 74 | if (line.match(/\@.*\s/)) { 75 | throw new Error("Syntax error in parameters"); 76 | } 77 | else { 78 | if (line.match(/\s/)) 79 | throw new Error("Syntax error in parameters"); 80 | if (!line.match(/^(postmaster|abuse)$/i)) 81 | throw new Error("Syntax error in address"); 82 | } 83 | } 84 | 85 | params.unshift(line); 86 | 87 | return params; 88 | } 89 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | // Log class 2 | 3 | var config = require('./config'); 4 | var plugins; 5 | var constants = require('./constants'); 6 | var util = require('util'); 7 | 8 | var logger = exports; 9 | 10 | logger.LOGDATA = 9; 11 | logger.LOGPROTOCOL = 8; 12 | logger.LOGDEBUG = 7; 13 | logger.LOGINFO = 6; 14 | logger.LOGNOTICE = 5; 15 | logger.LOGWARN = 4; 16 | logger.LOGERROR = 3; 17 | logger.LOGCRIT = 2; 18 | logger.LOGALERT = 1; 19 | logger.LOGEMERG = 0; 20 | 21 | var loglevel = logger.LOGWARN; 22 | 23 | var deferred_logs = []; 24 | 25 | logger.dump_logs = function () { 26 | while (deferred_logs.length > 0) { 27 | var log_item = deferred_logs.shift(); 28 | console.log(log_item); 29 | } 30 | } 31 | 32 | logger.log = function (data) { 33 | data = data.replace(/\n?$/, ""); 34 | // todo - just buffer these up (defer) until plugins are loaded 35 | if (plugins.plugin_list) { 36 | while (deferred_logs.length > 0) { 37 | var log_item = deferred_logs.shift(); 38 | plugins.run_hooks('log', logger, log_item); 39 | } 40 | plugins.run_hooks('log', logger, data); 41 | } 42 | else { 43 | deferred_logs.push(data); 44 | } 45 | } 46 | 47 | logger.log_respond = function (retval, msg, data) { 48 | // any other return code is irrelevant 49 | if (retval === constants.cont) { 50 | return console.log(data); 51 | } 52 | }; 53 | 54 | logger._init_loglevel = function () { 55 | var _loglevel = config.get('loglevel', 'nolog', 'value'); 56 | if (_loglevel) { 57 | var loglevel_num = parseInt(_loglevel); 58 | if (!loglevel_num || loglevel_num === NaN) { 59 | loglevel = logger[_loglevel.toUpperCase()]; 60 | } 61 | else { 62 | loglevel = loglevel_num; 63 | } 64 | if (!loglevel) { 65 | loglevel = logger.LOGWARN; 66 | } 67 | } 68 | // logger.log("Set log level to: " + loglevel); 69 | }; 70 | 71 | logger._init_loglevel(); 72 | 73 | var level, key; 74 | for (key in logger) { 75 | if(logger.hasOwnProperty(key)) { 76 | if (key.match(/^LOG\w/)) { 77 | level = key.slice(3); 78 | logger[key.toLowerCase()] = (function(level, key) { 79 | return function() { 80 | if (loglevel < logger[key]) 81 | return; 82 | var str = "[" + level + "] "; 83 | for (var i = 0; i < arguments.length; i++) { 84 | var data = arguments[i]; 85 | if (typeof(data) === 'object') { 86 | str += util.inspect(data); 87 | } 88 | else { 89 | str += data; 90 | } 91 | } 92 | logger.log(str); 93 | } 94 | })(level, key); 95 | } 96 | } 97 | } 98 | 99 | // load this down here so it sees all the logger methods compiled above 100 | plugins = require('./plugins'); 101 | -------------------------------------------------------------------------------- /docs/CoreConfig.md: -------------------------------------------------------------------------------- 1 | Core Configuration Files 2 | ======================== 3 | 4 | The Haraka core reads some configuration files to determine a few actions: 5 | 6 | * loglevel 7 | 8 | Can contain either a number or a string. See the top of logger.js for the 9 | different levels available. 10 | 11 | * databytes 12 | 13 | Contains the maximum SIZE of an email that Haraka will receive. 14 | 15 | * plugins 16 | 17 | The list of plugins to load 18 | 19 | * smtp.ini 20 | 21 | Keys: 22 | 23 | * port - the port to use (default: 25) 24 | * listen\_address - default: 0.0.0.0 (i.e. all addresses) 25 | * inactivity\_time - how long to let clients idle in seconds (default: 300) 26 | * nodes - if [cluster][1] is available, specifies how 27 | many processes to fork off. Can be the string "cpus" to fork off as many 28 | children as there are CPUs (default: 0, which disables cluster mode) 29 | * user - optionally a user to drop privileges to. Can be a string or UID. 30 | * ignore_bad_plugins - If a plugin fails to compile by default Haraka will stop at load time. 31 | If, however, you wish to continue on without that plugin's facilities, then 32 | set this config option 33 | 34 | [1]: http://learnboost.github.com/cluster/ 35 | 36 | * me 37 | 38 | A name to use for this server. Used in received lines and elsewhere. Setup 39 | by default to be your hostname. 40 | 41 | * deny_includes_uuid 42 | 43 | Each connection and mail in Haraka includes a UUID which is also in most log 44 | messages. If you put a `1` in this file then every denied mail (either via 45 | DENY/5xx or DENYSOFT/4xx return codes) will include a note containing the 46 | uuid, making it easy to track problems back to the logs. 47 | 48 | * early\_talker\_delay 49 | 50 | If clients talk early we *punish* them with a delay of this many milliseconds 51 | default: 1000. 52 | 53 | * plugin_timeout 54 | 55 | Seconds to allow a plugin to run before the next hook is called automatically 56 | default: 30 57 | 58 | Note also that each plugin can have a `config/<plugin_name>.timeout` 59 | file specifying a per-plugin timeout. In this file you can set a timeout 60 | of 0 to mean that this plugin's hooks never time out. Use this with care. 61 | 62 | * cluster_modules 63 | 64 | A list of cluster modules to load. Use colons to separate parameters to be 65 | passed to the module/plugin. Typical example: 66 | 67 | repl:8888 68 | stats 69 | 70 | The above allows you to get stats on your setup via the repl on port 8888. 71 | 72 | * smtpgreeting 73 | 74 | The greeting line used when a client connects. This can be multiple lines 75 | if required (this may cause some connecting machines to fail - though 76 | usually only spam-bots). 77 | 78 | * outbound.concurrency_max 79 | 80 | Maximum concurrency to use when delivering mails outbound. Defaults to 100. 81 | 82 | * outbound.disabled 83 | 84 | Put a `1` in this file to temporarily disable outbound delivery. Useful to 85 | do while you're figuring out network issues, or just testing things. 86 | 87 | * outbound.bounce_message 88 | 89 | The bounce message should delivery of the mail fail. See the source of. The 90 | default is normally fine. Bounce messages contain a number of template 91 | replacement values which are best discovered by looking at the source code. 92 | -------------------------------------------------------------------------------- /configfile.js: -------------------------------------------------------------------------------- 1 | // Config file loader 2 | 3 | var fs = require('fs'); 4 | 5 | // for "ini" type files 6 | var regex = { 7 | section: /^\s*\[\s*([^\]]*)\s*\]\s*$/, 8 | param: /^\s*(\w+)\s*=\s*(.*)\s*$/, 9 | comment: /^\s*[;#].*$/, 10 | line: /^\s*(.*)\s*$/, 11 | blank: /^\s*$/ 12 | }; 13 | 14 | var cfreader = exports; 15 | 16 | cfreader.watch_files = true; 17 | cfreader._config_cache = {}; 18 | 19 | cfreader.read_config = function(name, type) { 20 | // Check cache first 21 | if (cfreader._config_cache[name]) { 22 | return cfreader._config_cache[name]; 23 | } 24 | 25 | 26 | // load config file 27 | var result = cfreader.load_config(name, type); 28 | 29 | if (cfreader.watch_files) { 30 | fs.unwatchFile(name); 31 | fs.watchFile(name, function (curr, prev) { 32 | // file has changed 33 | if (curr.mtime.getTime() !== prev.mtime.getTime()) { 34 | cfreader.load_config(name, type); 35 | } 36 | }); 37 | } 38 | 39 | return result; 40 | } 41 | 42 | cfreader.empty_config = function(type) { 43 | if (type === 'ini') { 44 | return { main: {} }; 45 | } 46 | else { 47 | return []; 48 | } 49 | }; 50 | 51 | cfreader.load_config = function(name, type) { 52 | 53 | if (type === 'ini') { 54 | result = cfreader.load_ini_config(name); 55 | } 56 | else { 57 | result = cfreader.load_flat_config(name); 58 | if (result && type !== 'list') { 59 | result = result[0]; 60 | if (/^\d+$/.test(result)) { 61 | result = parseInt(result); 62 | } 63 | } 64 | } 65 | 66 | cfreader._config_cache[name] = result; 67 | 68 | return result; 69 | }; 70 | 71 | cfreader.load_ini_config = function(name) { 72 | var result = cfreader.empty_config('ini'); 73 | var current_sect = result.main; 74 | 75 | var data = new String(fs.readFileSync(name)); 76 | var lines = data.split(/\r\n|\r|\n/); 77 | var match; 78 | 79 | lines.forEach( function(line) { 80 | if (regex.comment.test(line)) { 81 | return; 82 | } 83 | else if (regex.blank.test(line)) { 84 | return; 85 | } 86 | else if (match = regex.param.exec(line)) { 87 | if (/^\d+$/.test(match[2])) { 88 | current_sect[match[1]] = parseInt(match[2]); 89 | } 90 | else { 91 | current_sect[match[1]] = match[2]; 92 | } 93 | } 94 | else if (match = regex.section.exec(line)) { 95 | current_sect = result[match[1]] = {}; 96 | } 97 | else { 98 | // error ? 99 | }; 100 | }); 101 | 102 | return result; 103 | }; 104 | 105 | cfreader.load_flat_config = function(name) { 106 | var result = []; 107 | var data = new String(fs.readFileSync(name)); 108 | var lines = data.split(/\r\n|\r|\n/); 109 | 110 | lines.forEach( function(line) { 111 | var line_data; 112 | if (regex.comment.test(line)) { 113 | return; 114 | } 115 | else if (regex.blank.test(line)) { 116 | return; 117 | } 118 | else if (line_data = regex.line.exec(line)) { 119 | result.push(line_data[1]); 120 | } 121 | }); 122 | 123 | return result; 124 | }; 125 | -------------------------------------------------------------------------------- /plugins/data.uribl.js: -------------------------------------------------------------------------------- 1 | // Look up URLs in SURBL 2 | var url = require('url'); 3 | var dns = require('dns'); 4 | 5 | var two_level_tlds = {}; 6 | 7 | exports.hook_data = function (next, connection) { 8 | // enable mail body parsing 9 | connection.transaction.parse_body = 1; 10 | next(); 11 | } 12 | 13 | exports.hook_data_post = function (next, connection) { 14 | var zones = this.config.get('data.uribl.zones', 'list'); 15 | 16 | this.config.get('data.uribl.two_level_tlds', 'list').forEach(function (tld) { 17 | two_level_tlds[tld] = 1; 18 | }); 19 | 20 | // this.loginfo(two_level_tlds); 21 | 22 | var urls = {}; 23 | 24 | // this.loginfo(connection.transaction.body); 25 | 26 | extract_urls(urls, connection.transaction.body); 27 | 28 | var hosts = Object.keys(urls); 29 | 30 | var pending_queries = 0; 31 | var next_ran = 0; 32 | var plugin = this; 33 | 34 | for (var i=0,l=hosts.length; i < l; i++) { 35 | var host = hosts[i]; 36 | var match = host.match(/([^\.]+\.)?([^\.]+\.[^\.]+)$/); 37 | if (match && !host.match(/^\d+\.\d+\.\d+\.\d+$/)) { 38 | if (two_level_tlds[match[2]]) { 39 | host = match[0]; 40 | } 41 | else { 42 | host = match[2]; 43 | } 44 | } 45 | // Now query for "host".zone 46 | for (var i=0,l=zones.length; i < l; i++) { 47 | pending_queries++; 48 | this.logdebug("Looking up: " + host + '.' + zones[i]); 49 | dns.resolveTxt(host + '.' + zones[i], function (err, addresses) { 50 | pending_queries--; 51 | if (!err) { 52 | if (!next_ran) { 53 | next_ran++; 54 | return next(DENY, addresses); 55 | } 56 | } 57 | if (pending_queries === 0) { 58 | next(); 59 | } 60 | }); 61 | } 62 | } 63 | 64 | if (pending_queries === 0) { 65 | // we didn't execute any DNS queries 66 | next(); 67 | } 68 | } 69 | 70 | var numeric_ip = /\w{3,16}:\/+(\S+@)?(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)/g; 71 | var schemeless = /((?:www\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]+\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))(?!\w)/g; 72 | var schemed = /(\w{3,16}:\/+(?:\S+@)?([a-zA-Z0-9][a-zA-Z0-9\-.]+\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2})))(?!\w)/g; 73 | 74 | function extract_urls (urls, body) { 75 | // extract from body.bodytext 76 | var match; 77 | // extract numeric URIs 78 | // commented out because DBL doesn't allow numeric IP queries 79 | //while (match = numeric_ip.exec(body.bodytext)) { 80 | // var uri = url.parse(match[0]); 81 | // urls[uri.hostname.split(/\./).reverse().join('.')] = uri; 82 | //} 83 | 84 | // match plain hostname.tld 85 | while (match = schemeless.exec(body.bodytext)) { 86 | var uri = url.parse('http://' + match[1]); 87 | urls[uri.hostname] = uri; 88 | } 89 | 90 | // match http:// URI 91 | while (match = schemed.exec(body.bodytext)) { 92 | var uri = url.parse(match[1]); 93 | urls[uri.hostname] = uri; 94 | } 95 | 96 | for (var i=0,l=body.children.length; i < l; i++) { 97 | extract_urls(urls, body.children[i]); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /mailheader.js: -------------------------------------------------------------------------------- 1 | // An RFC 2822 email header parser 2 | var logger = require('./logger'); 3 | 4 | function Header (options) { 5 | this.headers = {}; 6 | this.headers_decoded = {}; 7 | this.header_list = []; 8 | this.options = options; 9 | }; 10 | 11 | exports.Header = Header; 12 | 13 | Header.prototype.parse = function (lines) { 14 | for (var i=0,l=lines.length; i < l; i++) { 15 | var line = lines[i]; 16 | if (line.match(/^[ \t]/)) { 17 | // continuation 18 | this.header_list[this.header_list.length - 1] += line; 19 | } 20 | else { 21 | this.header_list.push(line); 22 | } 23 | } 24 | 25 | for (var i=0,l=this.header_list.length; i < l; i++) { 26 | var match = this.header_list[i].match(/^([^:]*):\s*([\s\S]*)$/); 27 | if (match) { 28 | var key = match[1].toLowerCase(); 29 | var val = match[2]; 30 | 31 | this._add_header(key, val); 32 | } 33 | else { 34 | logger.logerror("Header did not look right: " + this.header_list[i]); 35 | } 36 | } 37 | }; 38 | 39 | function _decode_header (matched, encoding, cte, data) { 40 | cte = cte.toUpperCase(); 41 | 42 | switch(cte) { 43 | case 'Q': 44 | data = data.replace('_', ' '); 45 | data = data.replace(/=([A-F0-9][A-F0-9])/gi, function (ignore, code) { 46 | return String.fromCharCode(parseInt(code, 16)); 47 | }); 48 | break; 49 | case 'B': 50 | data = new Buffer(data, "base64").toString(); 51 | break; 52 | default: 53 | logger.logerror("Invalid header encoding type: " + cte); 54 | } 55 | 56 | // todo: convert with iconv if encoding != UTF-8 57 | 58 | return data; 59 | } 60 | 61 | function decode_header (val) { 62 | // Fold continuations 63 | val = val.replace(/\n[ \t]+/g, "\n "); 64 | 65 | // remove end carriage return 66 | val = val.replace(/\r?\n$/, ''); 67 | 68 | if (! (/=\?/.test(val)) ) { 69 | // no encoded stuff 70 | return val; 71 | } 72 | 73 | val = val.replace(/=\?([\w_-]+)\?([bqBQ])\?(.*?)\?=/g, _decode_header); 74 | 75 | return val; 76 | } 77 | 78 | Header.prototype.get = function (key) { 79 | return (this.headers[key.toLowerCase()] || []).join("\n"); 80 | }; 81 | 82 | Header.prototype.get_all = function (key) { 83 | return this.headers[key.toLowerCase()] || []; 84 | }; 85 | 86 | Header.prototype.get_decoded = function (key) { 87 | return (this.headers_decoded[key.toLowerCase()] || []).join("\n"); 88 | }; 89 | 90 | Header.prototype.remove = function (key) { 91 | key = key.toLowerCase(); 92 | delete this.headers[key]; 93 | delete this.headers_decoded[key]; 94 | 95 | this._remove_more(key); 96 | } 97 | 98 | Header.prototype._remove_more = function (key) { 99 | var key_len = key.length; 100 | var to_remove; 101 | for (var i=0,l=this.header_list.length; i < l; i++) { 102 | if (this.header_list[i].substring(0, key_len).toLowerCase() === key) { 103 | this.header_list.splice(i, 1); 104 | return this._remove_more(key); 105 | } 106 | } 107 | }; 108 | 109 | Header.prototype._add_header = function (key, value) { 110 | this.headers[key] = this.headers[key] || []; 111 | this.headers[key].push(value); 112 | this.headers_decoded[key] = this.headers_decoded[key] || []; 113 | this.headers_decoded[key].push(decode_header(value)); 114 | }; 115 | 116 | Header.prototype.add = function (key, value) { 117 | this._add_header(key.toLowerCase(), value); 118 | this.header_list.push(key + ': ' + value + '\n'); 119 | }; 120 | 121 | Header.prototype.lines = function () { 122 | return this.header_list; 123 | }; 124 | 125 | Header.prototype.toString = function () { 126 | return this.header_list.join("\n"); 127 | }; 128 | -------------------------------------------------------------------------------- /plugins/queue/smtp_forward.js: -------------------------------------------------------------------------------- 1 | // Forward to an SMTP server 2 | 3 | var os = require('os'); 4 | var sock = require('./line_socket'); 5 | 6 | exports.register = function () { 7 | this.register_hook('queue', 'smtp_forward'); 8 | }; 9 | 10 | var smtp_regexp = /^([0-9]{3})([ -])(.*)/; 11 | 12 | exports.smtp_forward = function (next, connection) { 13 | this.loginfo("smtp forwarding"); 14 | var smtp_config = this.config.get('smtp_forward.ini', 'ini'); 15 | var socket = new sock.Socket(); 16 | socket.connect(smtp_config.main.port, smtp_config.main.host); 17 | socket.setTimeout(300 * 1000); 18 | var self = this; 19 | var command = 'connect'; 20 | var response = []; 21 | // copy the recipients: 22 | var recipients = connection.transaction.rcpt_to.map(function(item) { return item }); 23 | var data_marker = 0; 24 | 25 | var send_data = function () { 26 | if (data_marker < connection.transaction.data_lines.length) { 27 | var wrote_all = socket.write(connection.transaction.data_lines[data_marker].replace(/^\./, '..').replace(/\r?\n/g, '\r\n')); 28 | data_marker++; 29 | if (wrote_all) { 30 | send_data(); 31 | } 32 | } 33 | else { 34 | socket.send_command('dot'); 35 | } 36 | } 37 | 38 | socket.send_command = function (cmd, data) { 39 | var line = cmd + (data ? (' ' + data) : ''); 40 | if (cmd === 'dot') { 41 | line = '.'; 42 | } 43 | self.logprotocol("Fwd C: " + line); 44 | this.write(line + "\r\n"); 45 | command = cmd.toLowerCase(); 46 | }; 47 | 48 | socket.on('timeout', function () { 49 | self.logerror("Ongoing connection timed out"); 50 | socket.end(); 51 | next(); 52 | }); 53 | socket.on('error', function (err) { 54 | self.logerror("Ongoing connection failed: " + err); 55 | // we don't deny on error - maybe another plugin can deliver 56 | next(); 57 | }); 58 | socket.on('connect', function () { 59 | }); 60 | socket.on('line', function (line) { 61 | var matches; 62 | self.logprotocol("S: " + line); 63 | if (matches = smtp_regexp.exec(line)) { 64 | var code = matches[1], 65 | cont = matches[2], 66 | rest = matches[3]; 67 | response.push(rest); 68 | if (cont === ' ') { 69 | if (code.match(/^[45]/)) { 70 | socket.send_command('QUIT'); 71 | return next(); // Fall through to other queue hooks here 72 | } 73 | switch (command) { 74 | case 'connect': 75 | socket.send_command('HELO', self.config.get('me')); 76 | break; 77 | case 'helo': 78 | socket.send_command('MAIL', 'FROM:' + connection.transaction.mail_from); 79 | break; 80 | case 'mail': 81 | socket.send_command('RCPT', 'TO:' + recipients.shift()); 82 | if (recipients.length) { 83 | // don't move to next state if we have more recipients 84 | return; 85 | } 86 | break; 87 | case 'rcpt': 88 | socket.send_command('DATA'); 89 | break; 90 | case 'data': 91 | send_data(); 92 | break; 93 | case 'dot': 94 | socket.send_command('QUIT'); 95 | next(OK); 96 | break; 97 | case 'quit': 98 | socket.end(); 99 | break; 100 | default: 101 | throw new Error("Unknown command: " + command); 102 | } 103 | } 104 | } 105 | else { 106 | // Unrecognised response. 107 | self.logerror("Unrecognised response from upstream server: " + line); 108 | socket.end(); 109 | return next(); 110 | } 111 | }); 112 | socket.on('drain', function() { 113 | self.logdebug("Drained"); 114 | if (command === 'dot') { 115 | send_data(); 116 | } 117 | }); 118 | }; 119 | 120 | -------------------------------------------------------------------------------- /address.js: -------------------------------------------------------------------------------- 1 | // a class encapsulating an email address as per RFC-2821 2 | 3 | var logger = require('./logger'); 4 | 5 | var qchar = /([^a-zA-Z0-9!#\$\%\&\x27\*\+\x2D\/=\?\^_`{\|}~.])/; 6 | 7 | function Address (user, host) { 8 | var match = /^<(.*)>$/.exec(user); 9 | if (match) { 10 | this.original = user; 11 | this.parse(match[1]); 12 | } 13 | else if (!host) { 14 | this.original = user; 15 | this.parse(user); 16 | } 17 | else { 18 | this.original = user + '@' + host; 19 | this.user = user; 20 | this.host = host; 21 | } 22 | } 23 | 24 | 25 | exports.atom_expr = /[a-zA-Z0-9!#%&*+=?^_`{|}~\$\x27\x2D\/]+/; 26 | exports.address_literal_expr = 27 | /(?:\[(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|IPv6:[0-9A-Fa-f:.]+)\])/; 28 | exports.subdomain_expr = /(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?)/; 29 | exports.domain_expr; 30 | exports.qtext_expr = /[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]/; 31 | exports.text_expr = /\\([\x01-\x09\x0B\x0C\x0E-\x7F])/; 32 | 33 | var domain_re, source_route_re, user_host_re, atoms_re, qt_re; 34 | 35 | exports.compile_re = function () { 36 | domain_re = exports.domain_expr ? exports.domain_expr 37 | : new RegExp ( 38 | exports.subdomain_expr.source + 39 | '(?:\.' + exports.subdomain_expr.source + ')*' 40 | ); 41 | 42 | if (!exports.domain_expr && exports.address_literal_expr) { 43 | // if address_literal_expr is set, add it in 44 | domain_re = new RegExp('(?:' + exports.address_literal_expr.source + 45 | '|' + domain_re.source + ')'); 46 | } 47 | 48 | source_route_re = new RegExp('^\@' + domain_re.source + '(?:,\@' + domain_re.source + ')*:'); 49 | 50 | user_host_re = new RegExp('^(.*)\@(' + domain_re.source + ')$'); 51 | 52 | atoms_re = new RegExp('^' + exports.atom_expr.source + '(\.' + exports.atom_expr.source + ')*'); 53 | 54 | qt_re = new RegExp('^"((' + exports.qtext_expr.source + '|' + exports.text_expr.source + ')*)"$'); 55 | } 56 | 57 | exports.compile_re(); 58 | 59 | Address.prototype.parse = function (addr) { 60 | // strip source route 61 | addr = addr.replace(source_route_re, ''); 62 | 63 | // empty addr is ok 64 | if (addr === '') { 65 | this.user = null; 66 | this.host = null; 67 | return; 68 | } 69 | 70 | // bare postmaster is permissible, perl RFC-2821 (4.5.1) 71 | if (addr.toLowerCase() === 'postmaster') { 72 | this.user = 'postmaster'; 73 | this.host = null; 74 | return; 75 | } 76 | 77 | var matches; 78 | 79 | if (!(matches = user_host_re.exec(addr))) { 80 | throw new Error("Failed to parse address: " + addr); 81 | } 82 | 83 | var localpart = matches[1]; 84 | var domainpart = matches[2]; 85 | 86 | if (atoms_re.test(localpart)) { 87 | // simple case, we are done 88 | this.user = localpart; 89 | // I'm lower-case'ing here. Not sure if that's the "right" thing 90 | // to do, as the original case could be useful (but then you can get 91 | // that from address.original I guess). 92 | this.host = domainpart.toLowerCase(); 93 | return; 94 | } 95 | else if (matches = qt_re.exec(localpart)) { 96 | localpart = matches[1]; 97 | this.user = localpart.replace(exports.text_expr, '$1', 'g'); 98 | this.host = domainpart.toLowerCase(); 99 | return; 100 | } 101 | else { 102 | throw new Error("Failed to parse address: " + addr); 103 | } 104 | } 105 | 106 | Address.prototype.isNull = function () { 107 | return this.user ? 0 : 1; 108 | } 109 | 110 | Address.prototype.format = function () { 111 | if (this.isNull()) { 112 | return '<>'; 113 | } 114 | 115 | var user = this.user.replace(qchar, '\\$1', 'g'); 116 | if (user != this.user) { 117 | return '<"' + user + '"' + (this.host ? ('@' + this.host) : '') + '>'; 118 | } 119 | return '<' + this.address() + '>'; 120 | } 121 | 122 | Address.prototype.address = function (set) { 123 | if (set) { 124 | this.parse(set); 125 | } 126 | return (this.user ? this.user : '') + (this.host ? ('@' + this.host) : ''); 127 | } 128 | 129 | Address.prototype.toString = function () { 130 | return this.format(); 131 | } 132 | 133 | exports.Address = Address; 134 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // smtp network server 2 | 3 | var net = require('./tls_server'); 4 | var logger = require('./logger'); 5 | var config = require('./config'); 6 | var conn = require('./connection'); 7 | var out = require('./outbound'); 8 | var plugins = require('./plugins'); 9 | var constants = require('./constants'); 10 | var os = require('os'); 11 | 12 | var cluster; 13 | try { cluster = require('cluster') } // cluster can be installed with npm 14 | catch (err) { 15 | logger.logdebug("no cluster available, running single-process"); 16 | } 17 | 18 | // Need these here so we can run hooks 19 | for (var key in logger) { 20 | if (key.match(/^log\w/)) { 21 | exports[key] = (function (key) { 22 | return function () { 23 | var args = ["[server] "]; 24 | for (var i=0, l=arguments.length; i`. 96 | Look there for information about how each plugin is configured, edit your 97 | `config/plugins` file, restart Haraka and enjoy! 98 | 99 | Feel free to write to the mailing list with any questions. Or use github 100 | "Issues". 101 | 102 | ### Running from git 103 | 104 | If you are unable to use npm to install Haraka, you can run from git by 105 | following these steps: 106 | 107 | First clone the repository: 108 | 109 | $ git clone https://github.com/baudehlo/Haraka.git 110 | $ cd Haraka 111 | 112 | Edit `config/plugins` and `config/smtp.ini` to specify the plugins and 113 | config you want. 114 | 115 | Finally run Haraka: 116 | 117 | $ node haraka.js 118 | 119 | ### Performance 120 | 121 | Haraka is fast, due to the nature of using the v8 Javascript engine, and 122 | it is scalable due to using async I/O everywhere. On my local system I have 123 | managed to scale it up to 5000 emails per second (with minimal plugins). 124 | 125 | I welcome other performance evaluations. 126 | 127 | ### License and Author 128 | 129 | Haraka is MIT licensed - see the LICENSE file for details. 130 | 131 | Haraka is a project started by Matt Sergeant, a 10 year veteran of the email 132 | and anti-spam world. Previous projects have been the project leader for 133 | SpamAssassin and a hacker on Qpsmtpd, a perl based mail server which is 134 | quite similar to Haraka (but not as fast due to perl being slower than 135 | Javascript). 136 | 137 | [1]: http://nodejs.org/ 138 | [2]: http://youtu.be/6twKXMAsPsw 139 | -------------------------------------------------------------------------------- /docs/Plugins.md: -------------------------------------------------------------------------------- 1 | Writing Haraka Plugins 2 | ======= 3 | 4 | All aspects of receiving an email in Haraka are controlled via plugins, to the 5 | extent that no mail will even be received unless you have a minimum of a 'rcpt' 6 | plugin and a 'queue' plugin. 7 | 8 | The 'rcpt' plugin is used to determine if a particular recipient should be 9 | allowed to be relayed or received for. The 'queue' plugin queue's the email 10 | somewhere - perhaps to disk, or perhaps to an onward SMTP server. 11 | 12 | Anatomy of a Plugin 13 | ------ 14 | 15 | Plugins in Haraka are simply Javascript files in the plugins/ directory. 16 | 17 | To enable a plugin, simply add its name to `config/plugins`. 18 | 19 | In order to hook into the "rcpt" event, simply create a method in exports 20 | to hook it: 21 | 22 | exports.hook_rcpt = function (next, connection, params) { 23 | // email address is in params[0] 24 | // do something with the address... then call: 25 | next(); 26 | }; 27 | 28 | We've introduced a couple of new concepts here, so let's go through them: 29 | 30 | * next - we need to call this when we are done processing or Haraka will 31 | hang. 32 | * exports - the plugin acts as an object (with access to "this" if you need it) 33 | but methods go directly into exports. 34 | 35 | The next() method is the most critical thing here - since Haraka is an event based 36 | SMTP server, we may need to go off and fetch network information before we 37 | can return a result. We can do that asynchronously and simply run next() 38 | when we are done, which allows Haraka to go on processing other clients while 39 | we fetch our information. 40 | 41 | See "The Next Function" below for more details. 42 | 43 | Logging 44 | ------ 45 | 46 | Plugins inherit all the logging methods of `logger.js`, which are: 47 | 48 | * logprotocol 49 | * logdebug 50 | * loginfo 51 | * lognotice 52 | * logwarn 53 | * logerror 54 | * logcrit 55 | * logalert 56 | * logemerg 57 | 58 | It should also be noted that if plugins throw an exception directly when in a 59 | hook the exception will be caught and generate a logcrit level error. However 60 | they will not be caught quite as gracefully if you are in async code within 61 | your plugin. Use error codes for that, log the error, and run your next() 62 | function appropriately. 63 | 64 | Multiple Hooks 65 | ----- 66 | 67 | You can hook the same event multipe times, to do that provide a register() 68 | method and hook it: 69 | 70 | exports.register = function() { 71 | this.register_hook('queue', 'try_queue_my_way'); 72 | this.register_hook('queue', 'try_queue_highway'); 73 | }; 74 | 75 | Then when the earlier hook calls `next()` (without parameters) it continues on 76 | to the next hook you registered to try that one. 77 | 78 | The Next Function 79 | ================= 80 | 81 | The next() function takes two optional parameters: `code` and `msg` 82 | 83 | The code is one of the below listed return values. The msg corresponds with 84 | the string to send to the client in case of a failure. Use an Array if you need 85 | to send back a multi-line response. The msg should NOT contain the code number 86 | - that is taken care of by the Haraka internals. 87 | 88 | Return Values 89 | ------------- 90 | 91 | These constants are compiled into your plugin when it is loaded, you do not 92 | need to define them: 93 | 94 | * CONT 95 | 96 | Continue and let other plugins handle this particular hook. This is the 97 | default if no parameters are given. 98 | 99 | * DENY 100 | 101 | Reject the mail with a 5xx error. 102 | 103 | * DENYSOFT 104 | 105 | Reject the mail with a 4xx error. 106 | 107 | * DENYDISCONNECT 108 | 109 | Reject the mail with a 5xx error and immediately disconnect. 110 | 111 | * DISCONNECT 112 | 113 | Simply immediately disconnect 114 | 115 | * OK 116 | 117 | Required by rcpt and queue plugins if we are to allow the email to be 118 | accepted, or the queue was successful, respectively. Once a plugin calls 119 | next(OK) no further plugins will run after it. 120 | 121 | 122 | Available Hooks 123 | ------------- 124 | 125 | These are just the name of the hook, with any parameter sent to it: 126 | 127 | * init_master - called when the main (master) process is started 128 | * init_child - called whenever a child process is started when using multiple "nodes" 129 | * lookup_rdns - called to look up the rDNS - return the rDNS via `next(OK, rdns)` 130 | * connect - called after we got rDNS 131 | * capabilities - called to get the ESMTP capabilities (such as STARTTLS) 132 | * unrecognized_command - called when the remote end sends a command we don't recognise 133 | * disconnect - called upon disconnect 134 | * helo (hostname) 135 | * ehlo (hostname) 136 | * quit 137 | * vrfy 138 | * noop 139 | * mail ([from, esmtp\_params]) 140 | * rcpt ([to, esmtp\_params]) 141 | * rcpt_ok (to) 142 | * data - called at the DATA command 143 | * data_post - called at the end-of-data marker 144 | * queue - called to queue the mail 145 | * queue_ok - called when a mail has been queued successfully 146 | * deny - called if a plugin returns one of DENY, DENYSOFT or DENYDISCONNECT 147 | * get_mx - called when sending outbound mail to lookup the MX record 148 | * bounce - called when sending outbound mail if the mail would bounce 149 | 150 | The `rcpt` hook is slightly special. If we have a plugin (prior to rcpt) that 151 | sets the `connection.relaying = true` flag, then we do not need any rcpt 152 | hooks, or if we do, none of them need call `next(OK)`. However if 153 | `connection.relaying` remains `false` (as is the default - you don't want an 154 | open relay!), then one rcpt plugin MUST return `next(OK)` or your sender 155 | will receive the error message "I cannot deliver for that user". The most 156 | obvious choice for this activity is the `rcpt_to.in_host_list` plugin, which 157 | lists the domains for which you wish to receive email. 158 | 159 | If a rcpt plugin DOES call `next(OK)` then the `rcpt_ok` hook is run. This 160 | is primarily used by the queue/smtp_proxy plugin which needs to run after 161 | all rcpt hooks. 162 | 163 | Further Reading 164 | -------------- 165 | 166 | Now you want to read about the Connection object. 167 | 168 | 169 | -------------------------------------------------------------------------------- /plugins/lookup_rdns.strict.js: -------------------------------------------------------------------------------- 1 | // check rdns against forward 2 | 3 | var dns = require('dns'); 4 | 5 | // _dns_error handles err from node.dns callbacks. It will always call next() 6 | // with a DENYDISCONNECT for this plugin. 7 | function _dns_error(next, err, host, plugin, nxdomain, dnserror) { 8 | switch (err.code) { 9 | case dns.NXDOMAIN: 10 | plugin.loginfo('could not find a address for ' + host + 11 | '. Disconnecting.'); 12 | next(DENYDISCONNECT, 'Sorry we could not find address for ' + 13 | host + '. ' + nxdomain); 14 | break; 15 | 16 | default: 17 | plugin.loginfo('encountered an error when looking up ' + 18 | host + '. Disconnecting.'); 19 | next(DENYDISCONNECT, 'Sorry we encountered an error when ' + 20 | 'looking up ' + host + '. ' + dnserror); 21 | break; 22 | } 23 | } 24 | 25 | function _in_whitelist(plugin, address) { 26 | var domain = address.toLowerCase(); 27 | var host_list = 28 | plugin.config.get('lookup_rdns.strict.whitelist', 'list'); 29 | var host_list_regex = 30 | plugin.config.get('lookup_rdns.strict.whitelist_regex', 'list'); 31 | 32 | plugin.loginfo("Checking if " + address + " is in the " + 33 | "lookup_rdns.strict.whitelist files"); 34 | 35 | var i; 36 | for (i in host_list) { 37 | plugin.logdebug("checking " + domain + " against " + host_list[i]); 38 | 39 | if (host_list[i].toLowerCase() === domain) { 40 | plugin.logdebug("Allowing " + domain); 41 | return 1; 42 | } 43 | } 44 | 45 | for (i in host_list_regex) { 46 | plugin.logdebug("checking " + domain + " against " + 47 | host_list_regex[i]); 48 | 49 | var regex = new RegExp ('^' + host_list_regex[i] + '$', 'i'); 50 | 51 | if (domain.match(regex)) { 52 | plugin.logdebug("Allowing " + domain); 53 | return 1; 54 | } 55 | } 56 | 57 | return 0; 58 | } 59 | 60 | exports.hook_lookup_rdns = function (next, connection) { 61 | var plugin = this; 62 | var total_checks = 0; 63 | var called_next = 0; 64 | var timeout_id = 0; 65 | var config = this.config.get('lookup_rdns.strict.ini', 'ini'); 66 | var rdns = ''; 67 | var fwd_nxdomain = config.forward && (config.forward['nxdomain'] || ''); 68 | var fwd_dnserror = config.forward && (config.forward['dnserror'] || ''); 69 | var rev_nxdomain = config.reverse && (config.reverse['nxdomain'] || ''); 70 | var rev_dnserror = config.reverse && (config.reverse['dnserror'] || ''); 71 | var nomatch = config.general && (config.general['nomatch'] || ''); 72 | var timeout = config.general && (config.general['timeout'] || ''); 73 | var timeout_msg = config.general && (config.general['timeout_msg'] || ''); 74 | 75 | timeout_id = setTimeout(function () { 76 | if (!called_next) { 77 | plugin.loginfo('timed out when looking up ' + 78 | connection.remote_ip + '. Disconnecting.'); 79 | called_next++; 80 | 81 | if (_in_whitelist(plugin, connection.remote_ip)) { 82 | next(OK, connection.remote_ip); 83 | } else { 84 | next(DENYDISCONNECT, timeout_msg); 85 | } 86 | } 87 | }, timeout * 1000); 88 | 89 | dns.reverse(connection.remote_ip, function (err, domains) { 90 | if (err) { 91 | if (!called_next) { 92 | called_next++; 93 | 94 | if (_in_whitelist(plugin, connection.remote_ip)) { 95 | next(OK, connection.remote_ip); 96 | } else { 97 | _dns_error(next, err, connection.remote_ip, plugin, 98 | rev_nxdomain, rev_dnserror); 99 | } 100 | } 101 | } else { 102 | // Anything this strange needs documentation. Since we are 103 | // checking M (A) addresses for N (PTR) records, we need to 104 | // keep track of our total progress. That way, at the end, 105 | // we know to send an error of nothing has been found. Also, 106 | // on err, this helps us figure out if we still have more to check. 107 | total_checks = domains.length; 108 | 109 | // Now we should make sure that the reverse response matches 110 | // the forward address. Almost no one will have more than one 111 | // PTR record for a domain, however, DNS protocol does not 112 | // restrict one from having multiple PTR records for the same 113 | // address. So here we are, dealing with that case. 114 | domains.forEach(function (rdns) { 115 | dns.resolve4(rdns, function (err, addresses) { 116 | total_checks--; 117 | 118 | if (err) { 119 | if (!called_next && !total_checks) { 120 | called_next++; 121 | 122 | if (_in_whitelist(plugin, rdns)) { 123 | next(OK, rdns); 124 | } else { 125 | _dns_error(next, err, rdns, plugin, 126 | fwd_nxdomain, fwd_dnserror); 127 | } 128 | } 129 | } else { 130 | for (var i = 0; i < addresses.length ; i++) { 131 | if (addresses[i] === connection.remote_ip) { 132 | // We found a match, call next() and return 133 | if (!called_next) { 134 | called_next++; 135 | return next(OK, rdns); 136 | } 137 | } 138 | } 139 | 140 | if (!called_next && !total_checks) { 141 | called_next++; 142 | if (_in_whitelist(plugin, rdns)) { 143 | next(OK, rdns); 144 | } else { 145 | next(DENYDISCONNECT, nomatch); 146 | } 147 | } 148 | } 149 | }); 150 | }); 151 | } 152 | }); 153 | }; 154 | -------------------------------------------------------------------------------- /tls_server.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------------------------*/ 2 | /* Obtained from http://js.5sh.net/starttls.js on 8/18/2011. */ 3 | /*----------------------------------------------------------------------------------------------*/ 4 | 5 | var tls = require('tls'); 6 | var crypto = require('crypto'); 7 | var util = require('util'); 8 | var net = require('net'); 9 | var events = require('events'); 10 | var stream = require('stream'); 11 | var log = require('./logger'); 12 | 13 | // provides a common socket for attaching 14 | // and detaching from either main socket, or crypto socket 15 | function pluggableStream(socket) { 16 | stream.Stream.call(this); 17 | this.readable = this.writable = true; 18 | this._writeState = true; 19 | this._pending = []; 20 | this._pendingCallbacks = []; 21 | if (socket) 22 | this.attach(socket); 23 | } 24 | 25 | util.inherits(pluggableStream, stream.Stream); 26 | util.inherits(pluggableStream, events.EventEmitter); 27 | 28 | pluggableStream.prototype.pipe = function (socket) { 29 | this.on('data', function (data) { 30 | if (socket.write) 31 | socket.write(data); 32 | }); 33 | }; 34 | 35 | pluggableStream.prototype.attach = function (socket) { 36 | var self = this; 37 | self.targetsocket = socket; 38 | self.targetsocket.on('data', function (data) { 39 | self.emit('data', data); 40 | }); 41 | self.targetsocket.on('connect', function (a, b) { 42 | self.emit('connect', a, b); 43 | }); 44 | self.targetsocket.on('secureConnection', function (a, b) { 45 | self.emit('secureConnection', a, b); 46 | self.emit('secure', a, b); 47 | }); 48 | self.targetsocket.on('secure', function (a, b) { 49 | self.emit('secureConnection', a, b); 50 | self.emit('secure', a, b); 51 | }); 52 | self.targetsocket.on('end', function () { 53 | self.emit('end'); 54 | }); 55 | self.targetsocket.on('close', function () { 56 | self.emit('close'); 57 | }); 58 | self.targetsocket.on('drain', function () { 59 | self.emit('drain'); 60 | }); 61 | self.targetsocket.on('error', function (exception) { 62 | self.emit('error', exception); 63 | }); 64 | if (self.targetsocket.remotePort) { 65 | self.remotePort = self.targetsocket.remotePort; 66 | } 67 | 68 | if (self.targetsocket.remoteAddress) { 69 | self.remoteAddress = self.targetsocket.remoteAddress; 70 | } 71 | 72 | }; 73 | 74 | pluggableStream.prototype.clean = function (data) { 75 | if (this.targetsocket && this.targetsocket.removeAllListeners) { 76 | this.targetsocket.removeAllListeners('data'); 77 | this.targetsocket.removeAllListeners('secureConnection'); 78 | this.targetsocket.removeAllListeners('secure'); 79 | this.targetsocket.removeAllListeners('end'); 80 | this.targetsocket.removeAllListeners('close'); 81 | this.targetsocket.removeAllListeners('error'); 82 | this.targetsocket.removeAllListeners('drain'); 83 | } 84 | ; 85 | this.targetsocket = {}; 86 | this.targetsocket.write = function () { 87 | }; 88 | }; 89 | 90 | pluggableStream.prototype.write = function (data) { 91 | if (this.targetsocket.write) 92 | this.targetsocket.write(data); 93 | }; 94 | 95 | pluggableStream.prototype.setKeepAlive = function (/* true||false, timeout */) { 96 | }; 97 | 98 | pluggableStream.prototype.setNoDelay = function (/* true||false */) { 99 | }; 100 | 101 | function pipe(pair, socket) { 102 | pair.encrypted.pipe(socket); 103 | socket.pipe(pair.encrypted); 104 | 105 | pair.fd = socket.fd; 106 | var cleartext = pair.cleartext; 107 | cleartext.socket = socket; 108 | cleartext.encrypted = pair.encrypted; 109 | cleartext.authorized = false; 110 | 111 | function onerror(e) { 112 | if (cleartext._controlReleased) { 113 | cleartext.emit('error', e); 114 | } 115 | } 116 | 117 | function onclose() { 118 | socket.removeListener('error', onerror); 119 | socket.removeListener('close', onclose); 120 | } 121 | 122 | socket.on('error', onerror); 123 | socket.on('close', onclose); 124 | 125 | return cleartext; 126 | } 127 | 128 | function createServer(cb) { 129 | var serv = net.createServer(function (cryptoSocket) { 130 | 131 | var socket = new pluggableStream(cryptoSocket); 132 | socket.cryptoSocket = cryptoSocket; 133 | 134 | socket.upgrade = function (options) { 135 | log.logdebug("Upgrading to TLS"); 136 | 137 | socket.clean(); 138 | socket.cryptoSocket.removeAllListeners('data'); 139 | var sslcontext = crypto.createCredentials(options); 140 | 141 | var pair = tls.createSecurePair(sslcontext, true, false, false); 142 | 143 | var cleartext = pipe(pair, socket.cryptoSocket); 144 | 145 | pair.on('secure', function() { 146 | var verifyError = (pair.ssl || pair._ssl).verifyError(); 147 | 148 | log.logdebug("TLS secured."); 149 | if (verifyError) { 150 | cleartext.authorized = false; 151 | cleartext.authorizationError = verifyError; 152 | } else { 153 | cleartext.authorized = true; 154 | } 155 | 156 | }); 157 | 158 | cleartext._controlReleased = true; 159 | 160 | socket.cleartext = cleartext; 161 | 162 | socket.attach(socket.cleartext); 163 | }; 164 | 165 | cb(socket); 166 | }); 167 | 168 | return serv; 169 | } 170 | 171 | function connect(port, host, cb) { 172 | var cryptoSocket = new net.Stream(); 173 | 174 | cryptoSocket.connect(port, host); 175 | 176 | var socket = new pluggableStream(cryptoSocket); 177 | socket.cryptoSocket = cryptoSocket; 178 | 179 | socket.upgrade = function (options) { 180 | socket.clean(); 181 | socket.cryptoSocket.removeAllListeners('data'); 182 | var sslcontext = crypto.createCredentials(options); 183 | 184 | var pair = tls.createSecurePair(sslcontext, false); 185 | 186 | socket.pair = pair; 187 | 188 | var cleartext = pipe(pair, socket.cryptoSocket); 189 | 190 | pair.on('secure', function() { 191 | var verifyError = (pair.ssl || pair._ssl).verifyError(); 192 | 193 | log.logdebug("client TLS secured."); 194 | if (verifyError) { 195 | cleartext.authorized = false; 196 | cleartext.authorizationError = verifyError; 197 | } else { 198 | cleartext.authorized = true; 199 | } 200 | 201 | if (cb) cb(); 202 | }); 203 | 204 | cleartext._controlReleased = true; 205 | socket.cleartext = cleartext; 206 | socket.attach(socket.cleartext); 207 | 208 | log.logdebug("client TLS upgrade in progress, awaiting secured."); 209 | }; 210 | 211 | return (socket); 212 | } 213 | 214 | exports.connect = connect; 215 | exports.createConnection = connect; 216 | exports.Server = createServer; 217 | exports.createServer = createServer; -------------------------------------------------------------------------------- /plugins/spamassassin.js: -------------------------------------------------------------------------------- 1 | // Call spamassassin via spamd 2 | 3 | // Config is in spamassassin.ini 4 | // Valid keys: 5 | // reject_threshold=N - score at which to reject the mail 6 | // Default: don't reject mail 7 | // munge_subject_threshold=N - score at which to munge the subject 8 | // Default: don't munge the subject 9 | // subject_prefix=str - prefix to use when munging the subject. 10 | // Default: *** SPAM *** 11 | // spamd_socket=[/path|host:port] 12 | // - Default: localhost:783 13 | // spamd_user=str - username to pass to spamd 14 | // Default: same as current user 15 | // max_size=N - don't scan mails bigger than this 16 | // Default: 500000 17 | // old_headers_action=[rename|drop|keep] 18 | // - if old X-Spam-* headers are in the email, what do 19 | // we do with them? Default is to rename them 20 | // X-Old-Spam-*. "drop" will delete them. "keep" will 21 | // keep them (new X-Spam-* headers appear lower down 22 | // in the headers then). 23 | // 24 | 25 | var sock = require('./line_socket'); 26 | 27 | var defaults = { 28 | spamd_socket: 'localhost:783', 29 | max_size: 500000, 30 | old_headers_action: "rename", 31 | subject_prefix: "*** SPAM ***", 32 | }; 33 | 34 | exports.hook_data_post = function (next, connection) { 35 | var config = this.config.get('spamassassin.ini', 'ini'); 36 | var plugin = this; 37 | 38 | for (var key in defaults) { 39 | config.main[key] = config.main[key] || defaults[key]; 40 | } 41 | 42 | ['reject_threshold', 'munge_subject_threshold', 'max_size'].forEach( 43 | function (item) { 44 | if (config.main[item]) { 45 | config.main[item] = new Number(config.main[item]); 46 | } 47 | } 48 | ); 49 | 50 | if (connection.transaction.data_bytes > config.main.max_size) { 51 | return next(); 52 | } 53 | 54 | var socket = new sock.Socket(); 55 | if (config.main.spamd_socket.match(/\//)) { 56 | // assume unix socket 57 | socket.connect(config.main.spamd_socket); 58 | } 59 | else { 60 | var hostport = config.main.spamd_socket.split(/:/); 61 | socket.connect(hostport[1], hostport[0]); 62 | } 63 | 64 | socket.setTimeout(300 * 1000); 65 | 66 | var username = config.main.spamd_user || process.getuid(); 67 | 68 | var data_marker = 0; 69 | 70 | var send_data = function () { 71 | if (data_marker < connection.transaction.data_lines.length) { 72 | var wrote_all = socket.write(connection.transaction.data_lines[data_marker]); 73 | data_marker++; 74 | if (wrote_all) { 75 | send_data(); 76 | } 77 | } 78 | else { 79 | socket.end("\r\n"); 80 | } 81 | }; 82 | 83 | socket.on('timeout', function () { 84 | plugin.logerror("spamd connection timed out"); 85 | socket.end(); 86 | next(); 87 | }); 88 | socket.on('error', function (err) { 89 | plugin.logerror("spamd connection failed: " + err); 90 | // we don't deny on error - maybe another plugin can deliver 91 | next(); 92 | }); 93 | socket.on('connect', function () { 94 | socket.write("SYMBOLS SPAMC/1.3\r\n", function () { 95 | socket.write("User: " + username + "\r\n\r\n", function () { 96 | socket.write("X-Envelope-From: " + 97 | connection.transaction.mail_from.address() 98 | + "\r\n", function () 99 | { 100 | send_data(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | 106 | var spamd_response = {}; 107 | var state = 'line0'; 108 | 109 | socket.on('line', function (line) { 110 | plugin.logprotocol("SA: " + line); 111 | line = line.replace(/\r?\n/, ''); 112 | if (state === 'line0') { 113 | spamd_response.line0 = line; 114 | state = 'response'; 115 | } 116 | else if (state === 'response') { 117 | if (line.match(/\S/)) { 118 | var matches; 119 | if (matches = line.match(/Spam: (True|False) ; (-?\d+\.\d) \/ (-?\d+\.\d)/)) { 120 | spamd_response.flag = matches[1]; 121 | spamd_response.hits = matches[2]; 122 | spamd_response.reqd = matches[3]; 123 | spamd_response.flag = spamd_response.flag === 'True' ? 'Yes' : 'No' 124 | } 125 | } 126 | else { 127 | state = 'tests'; 128 | } 129 | } 130 | else if (state === 'tests') { 131 | spamd_response.tests = line; 132 | socket.destroy(); 133 | } 134 | }); 135 | 136 | socket.on('end', function () { 137 | // Now we do stuff with the results... 138 | 139 | plugin.fixup_old_headers(config.main.old_headers_action, connection.transaction); 140 | 141 | if (spamd_response.flag === 'Yes') { 142 | connection.transaction.add_header('X-Spam-Flag', 'YES'); 143 | } 144 | connection.transaction.add_header('X-Spam-Status', spamd_response.flag + 145 | ', hits=' + spamd_response.hits + ' required=' + spamd_response.reqd + 146 | "\n\ttests=" + spamd_response.tests); 147 | 148 | var stars = Math.floor(spamd_response.hits); 149 | if (stars < 1) stars = 1; 150 | if (stars > 50) stars = 50; 151 | var stars_string = ''; 152 | for (var i = 0; i < stars; i++) { 153 | stars_string += '*'; 154 | } 155 | connection.transaction.add_header('X-Spam-Level', stars_string); 156 | 157 | plugin.loginfo("spamassassin returned: " + spamd_response.flag + ', ' + 158 | spamd_response.hits + '/' + spamd_response.reqd + 159 | " Reject at: " + config.main.reject_threshold); 160 | 161 | if (config.main.reject_threshold && (spamd_response.hits >= config.main.reject_threshold)) { 162 | return next(DENY, "spam score exceeded threshold"); 163 | } 164 | else if (config.main.munge_subject_threshold && (spamd_response.hits >= config.main.munge_subject_threshold)) { 165 | var subj = connection.transaction.header.get('Subject'); 166 | connection.transaction.remove_header('Subject'); 167 | connection.transaction.add_header('Subject', config.main.subject_prefix + " " + subj); 168 | } 169 | next(); 170 | }); 171 | 172 | }; 173 | 174 | exports.fixup_old_headers = function (action, transaction) { 175 | var headers = ['X-Spam-Flag', 'X-Spam-Status', 'X-Spam-Level']; 176 | 177 | switch (action) { 178 | case "keep": return; 179 | case "drop": for (var key in headers) { transaction.remove_header(key) } 180 | break; 181 | case "rename": 182 | default: 183 | for (var key in headers) { 184 | var old_val = transaction.header.get(key); 185 | if (old_val) { 186 | transaction.header.remove_header(key); 187 | transaction.header.add_header(key.replace(/X-/, 'X-Old-'), old_val); 188 | } 189 | } 190 | break; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /mailbody.js: -------------------------------------------------------------------------------- 1 | // Mail Body Parser 2 | var logger = require('./logger'); 3 | var Header = require('./mailheader').Header; 4 | var events = require('events'); 5 | var util = require('util'); 6 | 7 | var buf_siz = 65536; 8 | 9 | function Body (header, options) { 10 | this.header = header || new Header(); 11 | this.header_lines = []; 12 | this.options = options; 13 | this.bodytext = ''; 14 | this.children = []; // if multipart 15 | this.state = 'start'; 16 | this.buf = new Buffer(buf_siz); 17 | this.buf_fill = 0; 18 | } 19 | 20 | util.inherits(Body, events.EventEmitter); 21 | exports.Body = Body; 22 | 23 | Body.prototype.parse_more = function (line) { 24 | this["parse_" + this.state](line); 25 | } 26 | 27 | Body.prototype.parse_child = function (line) { 28 | // check for MIME boundary 29 | if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) { 30 | 31 | if (this.children[this.children.length -1].state === 'attachment') { 32 | var child = this.children[this.children.length - 1]; 33 | if (child.buf_fill > 0) { 34 | // see below for why we create a new buffer here. 35 | var to_emit = new Buffer(child.buf_fill); 36 | child.buf.copy(to_emit, 0, 0, child.buf_fill); 37 | this.emit('attachment_data', to_emit); 38 | } 39 | this.emit('attachment_end'); 40 | } 41 | 42 | if (line.substr(this.boundary.length + 2, 2) === '--') { 43 | // end 44 | this.state = 'end'; 45 | } 46 | else { 47 | var bod = new Body(new Header(), this.options); 48 | this.listeners('attachment_start').forEach(function (cb) { bod.on('attachment_start', cb) }); 49 | this.listeners('attachment_data' ).forEach(function (cb) { bod.on('attachment_data', cb) }); 50 | this.listeners('attachment_end' ).forEach(function (cb) { bod.on('attachment_end', cb) }); 51 | this.children.push(bod); 52 | bod.state = 'headers'; 53 | } 54 | return; 55 | } 56 | // Pass data into last child 57 | this.children[this.children.length - 1].parse_more(line); 58 | } 59 | 60 | Body.prototype.parse_headers = function (line) { 61 | if (/^\s*$/.test(line)) { 62 | // end of headers 63 | this.header.parse(this.header_lines); 64 | delete this.header_lines; 65 | this.state = 'start'; 66 | } 67 | else { 68 | this.header_lines.push(line); 69 | } 70 | } 71 | 72 | Body.prototype.parse_start = function (line) { 73 | var ct = this.header.get_decoded('content-type') || 'text/plain'; 74 | var enc = this.header.get_decoded('content-transfer-encoding') || '8bit'; 75 | var cd = this.header.get_decoded('content-disposition') || ''; 76 | 77 | if (!enc.match(/^base64|quoted-printable|[78]bit$/i)) { 78 | logger.logerror("Invalid CTE on email: " + enc + ", using 8bit"); 79 | enc = '8bit'; 80 | } 81 | enc = enc.replace(/^quoted-printable$/i, 'qp'); 82 | enc = enc.toLowerCase().split("\n").pop().trim(); 83 | 84 | this.decode_function = this["decode_" + enc]; 85 | this.ct = ct; 86 | 87 | if (/^text\//i.test(ct) && !/^attachment/i.test(cd) ) { 88 | this.state = 'body'; 89 | } 90 | else if (/^multipart\//i.test(ct)) { 91 | var match = ct.match(/boundary\s*=\s*["']?([^"';]+)["']?/i); 92 | this.boundary = match[1] || ''; 93 | this.state = 'multipart_preamble'; 94 | } 95 | else { 96 | var match = cd.match(/name\s*=\s*["']?([^'";]+)["']?/i); 97 | if (!match) { 98 | match = ct.match(/name\s*=\s*["']?([^'";]+)["']?/i); 99 | } 100 | var filename = match ? match[1] : ''; 101 | this.emit('attachment_start', ct, filename, this); 102 | this.buf_fill = 0; 103 | this.state = 'attachment'; 104 | this.decode_function = this["decode_bin_" + enc]; 105 | } 106 | 107 | this["parse_" + this.state](line); 108 | } 109 | 110 | Body.prototype.parse_end = function (line) { 111 | // ignore these lines - but we could store somewhere I guess. 112 | } 113 | 114 | Body.prototype.parse_body = function (line) { 115 | this.bodytext += this.decode_function(line); 116 | } 117 | 118 | Body.prototype.parse_multipart_preamble = function (line) { 119 | if (this.boundary) { 120 | if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) { 121 | if (line.substr(this.boundary.length + 2, 2) === '--') { 122 | // end 123 | return; 124 | } 125 | else { 126 | // next section 127 | var bod = new Body(new Header(), this.options); 128 | this.listeners('attachment_start').forEach(function (cb) { bod.on('attachment_start', cb) }); 129 | this.listeners('attachment_data' ).forEach(function (cb) { bod.on('attachment_data', cb) }); 130 | this.listeners('attachment_end' ).forEach(function (cb) { bod.on('attachment_end', cb) }); 131 | this.children.push(bod); 132 | bod.state = 'headers'; 133 | this.state = 'child'; 134 | return; 135 | } 136 | } 137 | } 138 | this.bodytext += this.decode_function(line); 139 | } 140 | 141 | Body.prototype.parse_attachment = function (line) { 142 | if (this.boundary) { 143 | if (line.substr(0, (this.boundary.length + 2)) === ('--' + this.boundary)) { 144 | if (line.substr(this.boundary.length + 2, 2) === '--') { 145 | // end 146 | return; 147 | } 148 | else { 149 | // next section 150 | this.state = 'headers'; 151 | return; 152 | } 153 | } 154 | } 155 | 156 | var buf = this.decode_function(line); 157 | //this.emit('attachment_data', buf); 158 | //return; 159 | 160 | if ((buf.length + this.buf_fill) > buf_siz) { 161 | // now we have to create a new buffer, because if we write this out 162 | // using async code, it will get overwritten under us. Creating a new 163 | // buffer eliminates that problem (at the expense of a malloc and a 164 | // memcpy()) 165 | var to_emit = new Buffer(this.buf_fill); 166 | this.buf.copy(to_emit, 0, 0, this.buf_fill); 167 | this.emit('attachment_data', to_emit); 168 | if (buf.length > buf_siz) { 169 | // this is an unusual case - the base64/whatever data is larger 170 | // than our buffer size, so we just emit it and reset the counter. 171 | this.emit('attachment_data', buf); 172 | this.buf_fill = 0; 173 | } 174 | else { 175 | buf.copy(this.buf); 176 | this.buf_fill = buf.length; 177 | } 178 | } 179 | else { 180 | buf.copy(this.buf, this.buf_fill); 181 | this.buf_fill += buf.length; 182 | } 183 | } 184 | 185 | Body.prototype.decode_bin_qp = function (line) { 186 | line = line.replace(/=$/, ''); 187 | var buf = new Buffer(line.length); 188 | var offset = 0; 189 | var match; 190 | while (match = line.match(/^(.*?)=([A-F0-9][A-F0-9])/)) { 191 | line = line.substr(match[0].length); 192 | offset += buf.write(match[1], offset); 193 | buf[offset++] = parseInt(match[2], 16); 194 | } 195 | if (line.length) { 196 | buf.write(line, offset); 197 | } 198 | return buf; 199 | } 200 | 201 | Body.prototype.decode_qp = function (line) { 202 | line = line.replace(/=\r?\n/, ''); 203 | line = line.replace(/=([A-F0-9][A-F0-9])/g, function (ignore, code) { 204 | return String.fromCharCode(parseInt(code, 16)); 205 | }); 206 | // TODO - figure out encoding and apply it 207 | var encoding = 'utf8'; 208 | 209 | return new Buffer(line, encoding); 210 | } 211 | 212 | Body.prototype.decode_bin_base64 = function (line) { 213 | return new Buffer(line, "base64"); 214 | } 215 | 216 | Body.prototype.decode_base64 = function (line) { 217 | // TODO - figure out encoding and apply it 218 | return new Buffer(line, "base64").toString(); 219 | } 220 | 221 | Body.prototype.decode_8bit = function (line) { 222 | return line; 223 | } 224 | 225 | Body.prototype.decode_bin_8bit = function (line) { 226 | return new Buffer(line); 227 | } 228 | 229 | Body.prototype.decode_7bit = Body.prototype.decode_8bit; 230 | Body.prototype.decode_bin_7bit = Body.prototype.decode_bin_8bit; 231 | -------------------------------------------------------------------------------- /plugins.js: -------------------------------------------------------------------------------- 1 | // load all defined plugins 2 | 3 | var logger = require('./logger'); 4 | var config = require('./config'); 5 | var constants = require('./constants'); 6 | var path = require('path'); 7 | var vm = require('vm'); 8 | var fs = require('fs'); 9 | var utils = require('./utils'); 10 | var util = require('util'); 11 | 12 | var plugin_paths = [path.join(__dirname, './plugins')]; 13 | if (process.env.HARAKA) { plugin_paths.unshift(path.join(process.env.HARAKA, 'plugins')); } 14 | 15 | // These are the hooks that qpsmtpd implements - I should get around 16 | // to supporting them all some day... :-/ 17 | var regular_hooks = { 18 | 'connect':1, 19 | 'pre-connection': 1, 20 | 'connect': 1, 21 | 'ehlo_parse': 1, 22 | 'ehlo': 1, 23 | 'helo_parse': 1, 24 | 'helo': 1, 25 | 'auth_parse': 1, 26 | 'auth': 1, 27 | 'auth-plain': 1, 28 | 'auth-login': 1, 29 | 'auth-cram-md5': 1, 30 | 'rcpt_parse': 1, 31 | 'rcpt_pre': 1, 32 | 'rcpt': 1, 33 | 'mail_parse': 1, 34 | 'mail': 1, 35 | 'mail_pre': 1, 36 | 'data': 1, 37 | 'data_headers_end': 1, 38 | 'data_post': 1, 39 | 'queue_pre': 1, 40 | 'queue': 1, 41 | 'queue_post': 1, 42 | 'vrfy': 1, 43 | 'noop': 1, 44 | 'quit': 1, 45 | 'reset_transaction': 1, 46 | 'disconnect': 1, 47 | 'unrecognized_command': 1, 48 | 'help': 1 49 | }; 50 | 51 | function Plugin(name) { 52 | this.name = name; 53 | this.timeout = config.get(name + '.timeout', 'nolog'); 54 | if (this.timeout === null) { 55 | this.timeout = config.get('plugin_timeout', 'nolog') || 30; 56 | } 57 | else { 58 | logger.logdebug("plugin " + name + " set timeout to: " + this.timeout + "s"); 59 | } 60 | var full_paths = [] 61 | plugin_paths.forEach(function (pp) { 62 | full_paths.push(path.resolve(pp, name) + '.js'); 63 | }); 64 | this.full_paths = full_paths; 65 | this.config = config; 66 | this.hooks = {}; 67 | }; 68 | 69 | Plugin.prototype.register_hook = function(hook_name, method_name) { 70 | this.hooks[hook_name] = this.hooks[hook_name] || []; 71 | this.hooks[hook_name].push(method_name); 72 | 73 | logger.logdebug("registered hook " + hook_name + " to " + this.name + "." + method_name); 74 | } 75 | 76 | Plugin.prototype.register = function () {}; // noop 77 | 78 | // copy logger methods into Plugin: 79 | 80 | for (var key in logger) { 81 | if (key.match(/^log\w/)) { 82 | // console.log("adding Plugin." + key + " method"); 83 | Plugin.prototype[key] = (function (key) { 84 | return function () { 85 | var args = ["[" + this.name + "] "]; 86 | for (var i=0, l=arguments.length; i\ 136 | \ 137 | Haraka Mail Graphs\ 138 | \ 140 | \ 141 | \ 142 | \ 175 |

Haraka Mail Graphs

\ 176 |
\ 177 | Year\ 178 | Month\ 179 | Week\ 180 | Day\ 181 | Hour\ 182 |
\ 183 |
\ 184 |
\ 185 |
\ 186 | \ 187 | \ 188 | '); 189 | }; 190 | 191 | exports.handle_data = function (res, parsed) { 192 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 193 | var distance; 194 | // this.loginfo("query period: " + (parsed.query.period || 'day')); 195 | switch (parsed.query.period) { 196 | case 'year': 197 | distance = (86400000 * 365); 198 | break; 199 | case 'month': 200 | distance = (86400000 * 7 * 4); // ok, so 4 weeks 201 | break; 202 | case 'week': 203 | distance = (86400000 * 7); 204 | break; 205 | case 'hour': 206 | distance = 3600000; 207 | break; 208 | case 'day': 209 | default: 210 | distance = 86400000; 211 | } 212 | 213 | var today = new Date().getTime(); 214 | var earliest = today - distance; 215 | var group_by = distance/width; // one data point per pixel 216 | 217 | res.write("Date," + utils.sort_keys(plugins).join(',') + "\n"); 218 | 219 | this.get_data(res, earliest, today, group_by); 220 | }; 221 | 222 | exports.get_data = function (res, earliest, today, group_by) { 223 | var next_stop = earliest + group_by; 224 | var aggregate = reset_agg(); 225 | var plugin = this; 226 | 227 | function write_to (data) { 228 | // plugin.loginfo(data); 229 | res.write(data + "\n"); 230 | } 231 | 232 | db.query(select, [earliest, next_stop], function (err, row) { 233 | if (err) { 234 | res.end(); 235 | return plugin.logerror("SELECT failed: " + err); 236 | } 237 | if (!row) { 238 | write_to(utils.ISODate(new Date(next_stop)) + ',' + 239 | utils.sort_keys(plugins).map(function(i){ return 1000 * 60 * (aggregate[i]/group_by) }).join(',') 240 | ); 241 | if (next_stop >= today) { 242 | return res.end(); 243 | } 244 | else { 245 | return process.nextTick(function () { 246 | plugin.get_data(res, next_stop, today, group_by); 247 | }); 248 | } 249 | } 250 | // plugin.loginfo("got: " + row.hits + ", " + row.plugin + " next_stop: " + next_stop); 251 | 252 | aggregate[row.plugin] = row.hits; 253 | }); 254 | }; 255 | 256 | var reset_agg = function () { 257 | var agg = {}; 258 | for (var p in plugins) { 259 | agg[p] = 0; 260 | } 261 | return agg; 262 | }; 263 | 264 | exports.handle_404 = function (res, parsed) { 265 | this.logerror("404: " + parsed.href); 266 | res.writeHead(404); 267 | res.end('No such file: ' + parsed.href); 268 | }; 269 | -------------------------------------------------------------------------------- /docs/Tutorial.md: -------------------------------------------------------------------------------- 1 | Writing Haraka Plugins 2 | ====================== 3 | 4 | Part of the joy of using Haraka as your main mail server is having a strong 5 | plugin based system which means you control all aspects of how your mail is 6 | processed, accepted, and delivered. 7 | 8 | Of course in order to control this you may at some point need to edit some 9 | sort of plugin file of your own to customise how things work. The good news 10 | is that writing plugins in Haraka is simple, even for novice coders. You 11 | just need a little knowledge of Javascript (and maybe some understanding of 12 | Node.js) and the world is your oyster. 13 | 14 | This tutorial will run through a simple plugin which allows you to have 15 | email addresses that expire in a short period of time. This is handy if you 16 | want a *disposable email address* to use to sign up for a web site that you 17 | don't wish to continually receive communication from. 18 | 19 | The Design 20 | ---------- 21 | 22 | In order to make this simple, we are going to simply let you have tagged 23 | email addresses such as `user-20120515@domain.com` which will expire on the 24 | 15th May, 2012. Haraka will then check the email has yet to expire, and 25 | reject mails to that address after the expiry date. If the address hasn't 26 | expired yet it will re-write the address to `user@domain.com` before onward 27 | delivery. 28 | 29 | What You Will Need 30 | ------------------ 31 | 32 | * Node.js and npm 33 | * Haraka 34 | * A text editor 35 | * [swaks][1] 36 | * A screwdriver 37 | 38 | [1]: http://jetmore.org/john/code/swaks/ 39 | 40 | Getting Started 41 | --------------- 42 | 43 | First install Haraka via npm if you haven't already: 44 | 45 | $ sudo npm -g install Haraka 46 | 47 | Now we can create our project directory to get started with: 48 | 49 | $ haraka -i /path/to/new_project 50 | 51 | Make sure you use a directory that doesn't exist for your project. 52 | 53 | Next, let's create a new plugin: 54 | 55 | $ haraka -c /path/to/new_project -p rcpt_to.disposable 56 | 57 | This should output a bunch of information about files it has created: 58 | 59 | Plugin rcpt_to.disposable created 60 | Now edit javascript in: /path/to/new_project/plugins/rcpt_to.disposable.js 61 | Add the plugin to config: /path/to/new_project/config/plugins 62 | And edit documentation in: /path/to/new_project/docs/plugins/rcpt_to.disposable.md 63 | 64 | So let's do the second part now - load up the `config/plugins` file and lets 65 | set this up to test things. Comment out most of the plugins, except for 66 | `rcpt_to.in_host_list` and add in our new plugin, and change the queue 67 | plugin to `test_queue`. The final file should look like this: 68 | 69 | # default list of plugins 70 | 71 | # block mails from known bad hosts (see config/dnsbl.zones for the DNS zones queried) 72 | #dnsbl 73 | 74 | # allow bad mail signatures from the config/data.signatures file. 75 | #data.signatures 76 | 77 | # block mail from some known bad HELOs - see config/helo.checks.ini for configuration 78 | #helo.checks 79 | 80 | # block mail from known bad email addresses you put in config/mail_from.blocklist 81 | #mail_from.blocklist 82 | 83 | # Only accept mail where the MAIL FROM domain is resolvable to an MX record 84 | #mail_from.is_resolvable 85 | 86 | # Allow dated tagged addresses 87 | rcpt_to.disposable 88 | 89 | # Only accept mail for your personal list of hosts 90 | rcpt_to.in_host_list 91 | 92 | # Queue mail via qmail-queue 93 | #queue/qmail-queue 94 | 95 | test_queue 96 | 97 | Remember that the ordering here is important - our new plugin has to come 98 | before `rcpt_to.in_host_list`. 99 | 100 | Now fire up your favourite editor and put the following into 101 | the `plugins/rcpt_to.disposable.js` file: 102 | 103 | exports.hook_rcpt = function (next, connection, params) { 104 | var rcpt = params[0]; 105 | this.loginfo("Got recipient: " + rcpt); 106 | next(); 107 | } 108 | 109 | All we are doing here is logging the fact that we got the recipient. 110 | 111 | Check this works. You'll need two terminal windows. In window 1: 112 | 113 | $ echo LOGDEBUG > config/loglevel 114 | $ echo myserver.com >> config/host_list 115 | $ sudo haraka -c /path/to/new_project 116 | 117 | And in window 2: 118 | 119 | $ swaks -h domain.com -t booya@myserver.com -f somewhere@example.com \ 120 | -s localhost -p 25 121 | 122 | In the logs you should see: 123 | 124 | [INFO] [rcpt_to.disposable] Got recipient: 125 | 126 | Which indicates everything is working. You should also have a file 127 | `/tmp/mail.eml` containing the email that swaks sent. 128 | 129 | Parsing Out The Date 130 | -------------------- 131 | 132 | Now lets check for emails with an expire date in them and turn them into 133 | `Date` objects. Edit your plugin file as follows: 134 | 135 | exports.hook_rcpt = function (next, connection, params) { 136 | var rcpt = params[0]; 137 | this.loginfo("Got recipient: " + rcpt); 138 | 139 | // Check user matches regex 'user-YYYYMMDD': 140 | var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user); 141 | if (!match) { 142 | return next(); 143 | } 144 | 145 | // get date - note Date constructor takes month-1 (i.e. Dec == 11). 146 | var expiry_date = new Date(match[2], match[3]-1, match[4]); 147 | 148 | this.loginfo("Email expires on: " + expiry_date); 149 | 150 | next(); 151 | } 152 | 153 | Start haraka again and pass it the following email via swaks: 154 | 155 | $ swaks -h domain.com -t booya-20120101@myserver.com \ 156 | -f somewhere@example.com -s localhost -p 25 157 | 158 | And you should see now in the logs: 159 | 160 | [INFO] [rcpt_to.disposable] Got recipient: 161 | [INFO] [rcpt_to.disposable] Email expires on: Sun, 01 Jan 2012 05:00:00 GMT 162 | 163 | The exact time may vary depending on your timezone, but it should be obvious 164 | we now have a date object, which we can now compare to the current date. 165 | 166 | Rejecting Expired Emails 167 | ------------------------ 168 | 169 | The next edit we have to do is to add in code to compare to the current date 170 | and reject expired emails. Again, this is very simple: 171 | 172 | exports.hook_rcpt = function (next, connection, params) { 173 | var rcpt = params[0]; 174 | this.loginfo("Got recipient: " + rcpt); 175 | 176 | // Check user matches regex 'user-YYYYMMDD': 177 | var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user); 178 | if (!match) { 179 | return next(); 180 | } 181 | 182 | // get date - note Date constructor takes month-1 (i.e. Dec == 11). 183 | var expiry_date = new Date(match[2], match[3]-1, match[4]); 184 | 185 | this.loginfo("Email expires on: " + expiry_date); 186 | 187 | var today = new Date(); 188 | 189 | if (expiry_date < today) { 190 | // If we get here, the email address has expired 191 | return next(DENY, "Expired email address"); 192 | } 193 | 194 | next(); 195 | } 196 | 197 | And we can easily check that with swaks (remember to restart Haraka): 198 | 199 | $ swaks -h foo.com -t booya-20110101@haraka.local -f somewhere@example.com \ 200 | -s localhost -p 25 201 | === Trying localhost:25... 202 | === Connected to localhost. 203 | <- 220 sergeant.org ESMTP Haraka 0.3 ready 204 | -> EHLO foo.com 205 | <- 250-Haraka says hi Unknown [127.0.0.1] 206 | <- 250-PIPELINING 207 | <- 250-8BITMIME 208 | <- 250 SIZE 500000 209 | -> MAIL FROM: 210 | <- 250 From address is OK 211 | -> RCPT TO: 212 | <** 550 Expired email address 213 | -> QUIT 214 | <- 221 closing connection. Have a jolly good day. 215 | === Connection closed with remote host. 216 | 217 | Now we need to do one more thing... 218 | 219 | Fixing Up Unexpired Emails 220 | -------------------------- 221 | 222 | The last thing we need to do, is if we have an email that isn't expired, we 223 | need to normalise it back to the real email address, because wherever we 224 | deliver this to is unlikely to recognise these new email addresses. 225 | 226 | Here's how our final plugin will look: 227 | 228 | exports.hook_rcpt = function (next, connection, params) { 229 | var rcpt = params[0]; 230 | this.loginfo("Got recipient: " + rcpt); 231 | 232 | // Check user matches regex 'user-YYYYMMDD': 233 | var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user); 234 | if (!match) { 235 | return next(); 236 | } 237 | 238 | // get date - note Date constructor takes month-1 (i.e. Dec == 11). 239 | var expiry_date = new Date(match[2], match[3]-1, match[4]); 240 | 241 | this.loginfo("Email expires on: " + expiry_date); 242 | 243 | var today = new Date(); 244 | 245 | if (expiry_date < today) { 246 | // If we get here, the email address has expired 247 | return next(DENY, "Expired email address"); 248 | } 249 | 250 | // now get rid of the extension: 251 | rcpt.user = match[1]; 252 | this.loginfo("Email address now: " + rcpt); 253 | 254 | next(); 255 | } 256 | 257 | And when we test this with an unexpired address via swaks: 258 | 259 | $ swaks -h foo.com -t booya-20120101@haraka.local \ 260 | -f somewhere@example.com -s localhost -p 25 261 | 262 | We get in the logs: 263 | 264 | [INFO] [rcpt_to.disposable] Got recipient: 265 | [INFO] [rcpt_to.disposable] Email expires on: Sun Jan 01 2012 00:00:00 GMT-0500 (EST) 266 | [INFO] [rcpt_to.disposable] Email address now: 267 | 268 | Which indicates that we have successfully modified the email address. 269 | 270 | Further Reading 271 | =============== 272 | 273 | There are many more features of the Haraka API to explore, including access 274 | to the body of the email and the headers, access to the HELO string, and 275 | implementing ESMTP extensions, among many others. 276 | 277 | There are two good places to read up on these. Firstly is the documentation 278 | in the Haraka "docs" directory. Start with the `Plugins.md` file, and work 279 | your way through the API from there. 280 | 281 | The second place is simply reading the source code for the plugins themselves. 282 | The plugins that Haraka ships with use almost all parts of the API and so 283 | should give you a good starting point if you want to implement a particular 284 | piece of functionality. Even the most complicated plugins are under 200 lines 285 | of code, so don't be intimidated by them! The simplest one is a mere 5 lines 286 | of code. 287 | -------------------------------------------------------------------------------- /dsn.js: -------------------------------------------------------------------------------- 1 | // RFC 3463 Enhanced Status Codes 2 | var enum_status_codes = [ 3 | [ // X.0.XXX Other or Undefined Status (unspecified) 4 | "Other undefined status", // X.0.0 5 | ], 6 | [ // X.1.XXX Addressing Status (addr_*) 7 | "Other address status", // X.1.0 8 | "Bad destination mailbox address", // X.1.1 9 | "Bad destination system address", // X.1.2 10 | "Bad destination mailbox address syntax", // X.1.3 11 | "Destination mailbox address ambiguous", // X.1.4 12 | "Destination address valid", // X.1.5 13 | "Destination mailbox has moved, No forwarding address", // X.1.6 14 | "Bad sender's mailbox address syntax", // X.1.7 15 | "Bad sender's system address", // X.1.8 16 | ], 17 | [ // X.2.XXX Mailbox Status (mbox_*) 18 | "Other or undefined mailbox status", // X.2.0 19 | "Mailbox disabled, not accepting messages", // X.2.1 20 | "Mailbox full", // X.2.2 21 | "Message length exceeds administrative limit", // X.2.3 22 | "Mailing list expansion problem", // X.2.4 23 | ], 24 | [ // X.3.XXX Mail System Status (sys_*) 25 | "Other or undefined mail system status", // X.3.0 26 | "Mail system full", // X.3.1 27 | "System not accepting network messages", // X.3.2 28 | "System not capable of selected features", // X.3.3 29 | "Message too big for system", // X.3.4 30 | "System incorrectly configured", // X.3.5 31 | ], 32 | [ // X.4.XXX Network and Routing Status (net_*) 33 | "Other or undefined network or routing status", // X.4.0 34 | "No answer from host", // X.4.1 35 | "Bad connection", // X.4.2 36 | "Directory server failure", // X.4.3 37 | "Unable to route", // X.4.4 38 | "Mail system congestion", // X.4.5 39 | "Routing loop detected", // X.4.6 40 | "Delivery time expired", // X.4.7 41 | ], 42 | [ // X.5.XXX Mail Delivery Protocol Status (proto_*) 43 | "Other or undefined protocol status", // X.5.0 44 | "Invalid command", // X.5.1 45 | "Syntax error", // X.5.2 46 | "Too many recipients", // X.5.3 47 | "Invalid command arguments", // X.5.4 48 | "Wrong protocol version", // X.5.5 49 | ], 50 | [ // X.6.XXX Message Content or Media Status (media_*) 51 | "Other or undefined media error", // X.6.0 52 | "Media not supported", // X.6.1 53 | "Conversion required and prohibited", // X.6.2 54 | "Conversion required but not supported", // X.6.3 55 | "Conversion with loss performed", // X.6.4 56 | "Conversion failed", // X.6.5 57 | ], 58 | [ // X.7.XXX Security or Policy Status (sec_*) 59 | "Other or undefined security status", // X.7.0 60 | "Delivery not authorized, message refused", // X.7.1 61 | "Mailing list expansion prohibited", // X.7.2 62 | "Security conversion required but not possible", // X.7.3 63 | "Security features not supported", // X.7.4 64 | "Cryptographic failure", // X.7.5 65 | "Cryptographic algorithm not supported", // X.7.6 66 | "Message integrity failure", // X.7.7 67 | ] 68 | ]; 69 | 70 | function DSN(code, msg, def, subject, detail) { 71 | this.code = (/^[245]\d{2}/.exec(code)) ? code : null || def || 450; 72 | this.msg = msg; 73 | this.cls = parseInt(new String(this.code)[0]); 74 | this.sub = (enum_status_codes[subject]) ? subject : 0; 75 | this.det = (enum_status_codes[this.sub][detail]) ? detail : 0; 76 | this.default_msg = enum_status_codes[this.sub][this.det]; 77 | // Handle multi-line replies 78 | if (typeof(this.msg) === 'object' && this.msg.constructor === Array) { 79 | this.reply = new Array; 80 | var m; 81 | while (m = this.msg.shift()) { 82 | this.reply.push([this.cls, this.sub, this.det].join('.') + ' ' + m); 83 | } 84 | } else { 85 | this.reply = [this.cls, this.sub, this.det].join('.') + ' ' + (this.msg || this.default_msg); 86 | } 87 | } 88 | 89 | exports.unspecified = function(msg, code) { return new DSN(code, msg, 450, 0, 0); } 90 | 91 | // addr_* 92 | exports.addr_unspecified = function(msg, code) { return new DSN(code, msg, 450, 1, 0); } 93 | exports.addr_bad_dest_mbox = function(msg, code) { return new DSN(code, msg, 550, 1, 1); } 94 | exports.no_such_user = function(msg, code) { return new DSN(code, msg || 'No such user', 550, 1, 1); } 95 | exports.addr_bad_dest_system = function(msg, code) { return new DSN(code, msg, 550, 1, 2); } 96 | exports.addr_bad_dest_syntax = function(msg, code) { return new DSN(code, msg, 550, 1, 3); } 97 | exports.addr_dest_ambigous = function(msg, code) { return new DSN(code, msg, 450, 1, 4); } 98 | exports.addr_rcpt_ok = function(msg, code) { return new DSN(code, msg, 250, 1, 5); } 99 | exports.addr_mbox_moved = function(msg, code) { return new DSN(code, msg, 550, 1, 6); } 100 | exports.addr_bad_from_syntax = function(msg, code) { return new DSN(code, msg, 550, 1, 7); } 101 | exports.addr_bad_from_system = function(msg, code) { return new DSN(code, msg, 550, 1, 8); } 102 | 103 | // mbox_* 104 | exports.mbox_unspecified = function(msg, code) { return new DSN(code, msg, 450, 2, 0); } 105 | exports.mbox_disabled = function(msg, code) { return new DSN(code, msg, 550, 2, 1); } 106 | exports.mbox_full = function(msg, code) { return new DSN(code, msg, 450, 2, 2); } 107 | exports.mbox_msg_too_long = function(msg, code) { return new DSN(code, msg, 550, 2, 3); } 108 | exports.mbox_list_expansion_problem = function(msg, code) { return new DSN(code, msg, 450, 2, 4); } 109 | 110 | // sys_* 111 | exports.sys_unspecified = function(msg, code) { return new DSN(code, msg, 450, 3, 0); } 112 | exports.sys_disk_full = function(msg, code) { return new DSN(code, msg, 450, 3, 1); } 113 | exports.sys_not_accepting_mail = function(msg, code) { return new DSN(code, msg, 450, 3, 2); } 114 | exports.sys_not_supported = function(msg, code) { return new DSN(code, msg, 450, 3, 3); } 115 | exports.sys_msg_too_big = function(msg, code) { return new DSN(code, msg, 550, 3, 4); } 116 | exports.sys_incorrectly_configured = function(msg, code) { return new DSN(code, msg, 450, 3, 5); } 117 | 118 | // net_* 119 | exports.net_unspecified = function(msg, code) { return new DSN(code, msg, 450, 4, 0); } 120 | exports.net_no_answer = function(msg, code) { return new DSN(code, msg, 450, 4, 1); } 121 | exports.net_bad_connection = function(msg, code) { return new DSN(code, msg, 450, 4, 2); } 122 | exports.net_directory_server_failed = function(msg, code) { return new DSN(code, msg, 450, 4, 3); } 123 | exports.temp_resolver_failed = function(msg, code) { return new DSN(code, msg || 'Temporary address resolution failure', 450, 4, 3); } 124 | exports.net_unable_to_route = function(msg, code) { return new DSN(code, msg, 550, 4, 4); } 125 | exports.net_system_congested = function(msg, code) { return new DSN(code, msg, 450, 4, 5); } 126 | exports.net_routing_loop = function(msg, code) { return new DSN(code, msg, 550, 4, 6); } 127 | exports.too_many_hops = function(msg, code) { return new DSN(code, msg || 'Too many hops', 550, 4, 6); } 128 | exports.net_delivery_time_expired = function(msg, code) { return new DSN(code, msg, 550, 4, 7); } 129 | 130 | // proto_* 131 | exports.proto_unspecified = function(msg, code) { return new DSN(code, msg, 450, 5, 0); } 132 | exports.proto_invalid_command = function(msg, code) { return new DSN(code, msg, 550, 5, 1); } 133 | exports.proto_syntax_error = function(msg, code) { return new DSN(code, msg, 550, 5, 2); } 134 | exports.proto_too_many_rcpts = function(msg, code) { return new DSN(code, msg, 450, 5, 3); } 135 | exports.proto_invalid_cmd_args = function(msg, code) { return new DSN(code, msg, 550, 5, 4); } 136 | exports.proto_wrong_version = function(msg, code) { return new DSN(code, msg, 450, 5, 5); } 137 | 138 | // media_* 139 | exports.media_unspecified = function(msg, code) { return new DSN(code, msg, 450, 6, 0); } 140 | exports.media_unsupported = function(msg, code) { return new DSN(code, msg, 550, 6, 1); } 141 | exports.media_conv_prohibited = function(msg, code) { return new DSN(code, msg, 550, 6, 2); } 142 | exports.media_conv_unsupported = function(msg, code) { return new DSN(code, msg, 450, 6, 3); } 143 | exports.media_conv_lossy = function(msg, code) { return new DSN(code, msg, 450, 6, 4); } 144 | exports.media_conv_failed = function(msg, code) { return new DSN(code, msg, 450, 6, 5); } 145 | 146 | // sec_* 147 | exports.sec_unspecified = function(msg, code) { return new DSN(code, msg, 450, 7, 0); } 148 | exports.sec_unauthorized = function(msg, code) { return new DSN(code, msg, 550, 7, 1); } 149 | exports.bad_sender_ip = function(msg, code) { return new DSN(code, msg || 'Bad sender IP', 550, 7, 1); } 150 | exports.relaying_denied = function(msg, code) { return new DSN(code, msg || 'Relaying denied', 550, 7, 1); } 151 | exports.sec_list_expn_prohibited = function(msg, code) { return new DSN(code, msg, 550, 7, 2); } 152 | exports.sec_conv_failed = function(msg, code) { return new DSN(code, msg, 550, 7, 3); } 153 | exports.sec_feature_unsupported = function(msg, code) { return new DSN(code, msg, 550, 7, 4); } 154 | exports.sec_crypto_failure = function(msg, code) { return new DSN(code, msg, 550, 7, 5); } 155 | exports.sec_crypto_algo_unsupported = function(msg, code) { return new DSN(code, msg, 450, 7, 6); } 156 | exports.sec_msg_integrity_failure = function(msg, code) { return new DSN(code, msg, 550, 7, 7); } 157 | -------------------------------------------------------------------------------- /plugins/queue/smtp_proxy.js: -------------------------------------------------------------------------------- 1 | // Forward to an SMTP server as a proxy. 2 | // Opens the connection to the ongoing SMTP server at MAIL FROM time 3 | // and passes back any errors seen on the ongoing server to the originating server. 4 | 5 | var os = require('os'); 6 | var sock = require('./line_socket'); 7 | 8 | var smtp_regexp = /^([0-9]{3})([ -])(.*)/; 9 | 10 | // Local function to get an smtp_proxy connection. 11 | // This function will either choose one from the pool or make new one. 12 | function _get_smtp_proxy(self, next, connection) { 13 | var smtp_proxy = {}; 14 | 15 | if (connection.server.notes.smtp_proxy_pool && 16 | connection.server.notes.smtp_proxy_pool.length) { 17 | self.logdebug("using connection from the pool: (" + 18 | connection.server.notes.smtp_proxy_pool.length + ")"); 19 | 20 | smtp_proxy = connection.server.notes.smtp_proxy_pool.shift(); 21 | 22 | // We should just reset these things when we shift a connection off 23 | // since we have to setup stuff based on _this_ connection. 24 | smtp_proxy.response = []; 25 | smtp_proxy.recipient_marker = 0; 26 | smtp_proxy.pool_connection = 1; 27 | connection.notes.smtp_proxy = smtp_proxy; 28 | smtp_proxy.next = next; 29 | 30 | // Cleanup all old event listeners 31 | // Note, if new ones are added in the mail from handler, 32 | // please remove them here. 33 | smtp_proxy.socket.removeAllListeners('error'); 34 | smtp_proxy.socket.removeAllListeners('timeout'); 35 | smtp_proxy.socket.removeAllListeners('close'); 36 | smtp_proxy.socket.removeAllListeners('connect'); 37 | smtp_proxy.socket.removeAllListeners('line'); 38 | smtp_proxy.socket.removeAllListeners('drain'); 39 | } else { 40 | smtp_proxy.config = self.config.get('smtp_proxy.ini', 'ini'); 41 | smtp_proxy.socket = new sock.Socket(); 42 | smtp_proxy.socket.connect(smtp_proxy.config.main.port, 43 | smtp_proxy.config.main.host); 44 | smtp_proxy.socket.setTimeout((smtp_proxy.config.main.timeout) ? 45 | (smtp_proxy.config.main.timeout * 1000) : (300 * 1000)); 46 | smtp_proxy.command = 'connect'; 47 | smtp_proxy.response = []; 48 | smtp_proxy.recipient_marker = 0; 49 | smtp_proxy.pool_connection = 0; 50 | connection.notes.smtp_proxy = smtp_proxy; 51 | smtp_proxy.next = next; 52 | } 53 | 54 | if (connection.server.notes.active_proxy_conections >= 0) { 55 | connection.server.notes.active_proxy_conections++; 56 | } else { 57 | connection.server.notes.active_proxy_conections = 1; 58 | } 59 | 60 | self.logdebug("active proxy connections: (" + 61 | connection.server.notes.active_proxy_conections + ")"); 62 | 63 | return smtp_proxy; 64 | } 65 | 66 | // function will destroy an smtp_proxy and pull it out of the idle array 67 | function _destroy_smtp_proxy(self, connection, smtp_proxy) { 68 | var reset_active_connections = 0; 69 | var index; 70 | 71 | if (smtp_proxy && smtp_proxy.socket) { 72 | self.logdebug("destroying proxy connection"); 73 | smtp_proxy.socket.destroySoon(); 74 | smtp_proxy.socket = 0; 75 | reset_active_connections = 1; 76 | } 77 | 78 | // Unlink the connection from the proxy just in case we got here 79 | // without that happening already. 80 | if (connection && connection.notes.smtp_proxy) { 81 | delete connection.notes.smtp_proxy; 82 | } 83 | 84 | if (connection.server.notes.smtp_proxy_pool) { 85 | // Pull that smtp_proxy from the proxy pool. 86 | // Note we do not do this operation that often. 87 | index = connection.server.notes.smtp_proxy_pool.indexOf(smtp_proxy); 88 | if (index != -1) { 89 | connection.server.notes.smtp_proxy_pool.splice(index, 1); 90 | self.logdebug("pulling dead proxy connection from pool: (" + 91 | connection.server.notes.smtp_proxy_pool.length + ")"); 92 | } 93 | } 94 | 95 | if (reset_active_connections) { 96 | connection.server.notes.active_proxy_conections--; 97 | self.logdebug("active proxy connections: (" + 98 | connection.server.notes.active_proxy_conections + ")"); 99 | } 100 | 101 | return; 102 | } 103 | 104 | function _smtp_proxy_idle(self, connection) { 105 | var smtp_proxy = connection.notes.smtp_proxy; 106 | 107 | if (!(smtp_proxy)) { 108 | return; 109 | } 110 | 111 | if (connection.server.notes.smtp_proxy_pool) { 112 | connection.server.notes.smtp_proxy_pool.push(smtp_proxy); 113 | } else { 114 | connection.server.notes.smtp_proxy_pool = [ smtp_proxy ]; 115 | } 116 | 117 | connection.server.notes.active_proxy_conections--; 118 | 119 | self.logdebug("putting proxy connection back in pool: (" + 120 | connection.server.notes.smtp_proxy_pool.length + ")"); 121 | self.logdebug("active proxy connections: (" + 122 | connection.server.notes.active_proxy_conections + ")"); 123 | 124 | // Unlink this connection from the proxy now that it is back 125 | // in the pool. 126 | if (connection && connection.notes.smtp_proxy) { 127 | delete connection.notes.smtp_proxy; 128 | } 129 | 130 | return; 131 | } 132 | 133 | exports.hook_mail = function (next, connection, params) { 134 | this.loginfo("smtp proxying"); 135 | var self = this; 136 | var mail_from = params[0]; 137 | var data_marker = 0; 138 | var smtp_proxy = _get_smtp_proxy(self, next, connection); 139 | 140 | smtp_proxy.send_data = function () { 141 | if (data_marker < connection.transaction.data_lines.length) { 142 | var wrote_all = smtp_proxy.socket.write(connection.transaction.data_lines[data_marker].replace(/^\./, '..').replace(/\r?\n/g, '\r\n')); 143 | data_marker++; 144 | if (wrote_all) { 145 | smtp_proxy.send_data(); 146 | } 147 | } 148 | else { 149 | smtp_proxy.socket.send_command('dot'); 150 | } 151 | } 152 | 153 | // Add socket event listeners. 154 | // Note, if new ones are added here, please remove them in _get_smtp_proxy. 155 | 156 | smtp_proxy.socket.on('error', function (err) { 157 | self.logdebug("Ongoing connection failed: " + err); 158 | _destroy_smtp_proxy(self, connection, smtp_proxy); 159 | }); 160 | 161 | smtp_proxy.socket.on('timeout', function () { 162 | self.logdebug("Ongoing connection timed out"); 163 | _destroy_smtp_proxy(self, connection, smtp_proxy); 164 | }); 165 | 166 | smtp_proxy.socket.on('close', function (had_error) { 167 | self.logdebug("Ongoing connection closed"); 168 | _destroy_smtp_proxy(self, connection, smtp_proxy); 169 | }); 170 | 171 | smtp_proxy.socket.on('connect', function () {}); 172 | 173 | smtp_proxy.socket.send_command = function (cmd, data) { 174 | var line = cmd + (data ? (' ' + data) : ''); 175 | if (cmd === 'dot') { 176 | line = '.'; 177 | } 178 | self.logprotocol("Proxy C: " + line); 179 | this.write(line + "\r\n"); 180 | smtp_proxy.command = cmd.toLowerCase(); 181 | }; 182 | 183 | smtp_proxy.socket.on('line', function (line) { 184 | var matches; 185 | self.logprotocol("Proxy S: " + line); 186 | if (matches = smtp_regexp.exec(line)) { 187 | var code = matches[1], 188 | cont = matches[2], 189 | rest = matches[3]; 190 | smtp_proxy.response.push(rest); 191 | if (cont === ' ') { 192 | if (code.match(/^[45]/)) { 193 | if (smtp_proxy.command !== 'rcpt') { 194 | // errors are OK for rcpt, but nothing else 195 | // this can also happen if the destination server 196 | // times out, but that is okay. 197 | smtp_proxy.socket.send_command('RSET'); 198 | } 199 | return smtp_proxy.next(code.match(/^4/) ? 200 | DENYSOFT : DENY, smtp_proxy.response); 201 | } 202 | 203 | smtp_proxy.response = []; // reset the response 204 | 205 | switch (smtp_proxy.command) { 206 | case 'connect': 207 | smtp_proxy.socket.send_command('HELO', 208 | self.config.get('me')); 209 | break; 210 | case 'helo': 211 | smtp_proxy.socket.send_command('MAIL', 212 | 'FROM:' + mail_from); 213 | break; 214 | case 'mail': 215 | smtp_proxy.next(); 216 | break; 217 | case 'rcpt': 218 | smtp_proxy.next(); 219 | break; 220 | case 'data': 221 | smtp_proxy.next(); 222 | break; 223 | case 'dot': 224 | smtp_proxy.socket.send_command('RSET'); 225 | smtp_proxy.next(OK); 226 | break; 227 | case 'rset': 228 | _smtp_proxy_idle(self, connection); 229 | // We do not call next() here because many paths 230 | // lead to this conclusion, and next() is called 231 | // on a case-by-case basis. 232 | break; 233 | default: 234 | throw "Unknown command: " + smtp_proxy.command; 235 | } 236 | } 237 | } 238 | else { 239 | // Unrecognised response. 240 | self.logerror("Unrecognised response from upstream server: " + line); 241 | smtp_proxy.socket.send_command('RSET'); 242 | return smtp_proxy.next(DENYSOFT); 243 | } 244 | }); 245 | 246 | smtp_proxy.socket.on('drain', function() { 247 | self.logprotocol("Drained"); 248 | if (smtp_proxy.command === 'dot') { 249 | smtp_proxy.send_data(); 250 | } 251 | }); 252 | 253 | if (smtp_proxy.pool_connection) { 254 | smtp_proxy.socket.send_command('MAIL', 'FROM:' + mail_from); 255 | } 256 | }; 257 | 258 | exports.hook_rcpt_ok = function (next, connection, recipient) { 259 | if (!connection.notes.smtp_proxy) return next(); 260 | var smtp_proxy = connection.notes.smtp_proxy; 261 | smtp_proxy.next = next; 262 | smtp_proxy.socket.send_command('RCPT', 'TO:' + recipient); 263 | }; 264 | 265 | exports.hook_data = function (next, connection) { 266 | if (!connection.notes.smtp_proxy) return next(); 267 | var smtp_proxy = connection.notes.smtp_proxy; 268 | smtp_proxy.next = next; 269 | smtp_proxy.socket.send_command("DATA"); 270 | }; 271 | 272 | exports.hook_queue = function (next, connection) { 273 | if (!connection.notes.smtp_proxy) return next(); 274 | var smtp_proxy = connection.notes.smtp_proxy; 275 | smtp_proxy.command = 'dot'; 276 | smtp_proxy.next = next; 277 | smtp_proxy.send_data(); 278 | }; 279 | 280 | exports.hook_quit = function (next, connection) { 281 | if (!connection.notes.smtp_proxy) return next(); 282 | var smtp_proxy = connection.notes.smtp_proxy; 283 | smtp_proxy.next = next; 284 | smtp_proxy.socket.send_command("RSET"); 285 | smtp_proxy.next(OK); 286 | }; 287 | -------------------------------------------------------------------------------- /bin/haraka: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // this script takes inspiration from: 4 | // https://github.com/visionmedia/express/blob/master/bin/express 5 | // https://github.com/tnantoka/LooseLeaf/blob/master/bin/looseleaf 6 | 7 | 8 | var fs = require('fs'), 9 | path = require('path'), 10 | nopt = require("nopt"), 11 | os = require('os'), 12 | base = path.join(__dirname, '..'), 13 | ver = JSON.parse(fs.readFileSync(base+'/package.json', 'utf8'))['version'], 14 | knownOpts = { 15 | "version": Boolean, 16 | "verbose": Boolean, 17 | "help": [String, null], 18 | "configs": path, 19 | "install": path, 20 | "list": Boolean, 21 | "plugin": String, 22 | "force": Boolean, 23 | "qlist": Boolean, 24 | "qstat": Boolean, 25 | "qempty": Boolean, 26 | }, 27 | shortHands = { 28 | "v": ["--version"], 29 | "h": ["--help"], 30 | "c": ["--configs"], 31 | "i": ["--install"], 32 | "l": ["--list"], 33 | "p": ["--plugin"], 34 | "f": ["--force"], 35 | }, 36 | parsed = nopt(knownOpts, shortHands, process.argv, 2); 37 | 38 | 39 | var usage = [ 40 | "\033[32;40mHaraka.js\033[0m — A Node.js Email Server project", 41 | "Usage: haraka [options] [path]", 42 | "Options:", 43 | "\t-v, --version \t\tOutputs version number", 44 | "\t-h, --help \t\tOutputs this help message", 45 | "\t-h NAME \t\tShows help for NAME", 46 | "\t-c, --configs \t\tPath to your config directory", 47 | "\t-i, --install \t\tCopies the default configs to a specified dir", 48 | "\t-l, --list \t\tList the plugins bundled with Haraka", 49 | "\t-p, --plugin \t\tGenerate a new plugin with the given name", 50 | "\t-f, --force \t\tForce overwriting of old files", 51 | "\t--qlist \t\tList the outbound queue", 52 | "\t--qstat \t\tGet statistics on the outbound queue", 53 | ].join('\n'); 54 | 55 | 56 | var listPlugins = function (b, dir) { 57 | 58 | if (!dir) { dir = "plugins/"; } 59 | 60 | var plist = dir + "\n", 61 | subdirs = [], 62 | gl = path.join((b ? b : base), dir), 63 | pd = fs.readdirSync(gl); 64 | 65 | pd.forEach(function (p) { 66 | if (~p.search('.js')) { 67 | plist += "\t" + p.replace('.js', '') + "\n"; 68 | } else { 69 | subdirs.push(dir + p + "/"); 70 | } 71 | }); 72 | 73 | subdirs.forEach(function (s) { 74 | plist += "\n" + listPlugins(b, s); 75 | }); 76 | 77 | return plist; 78 | 79 | } 80 | 81 | 82 | // Show message when create 83 | function create(path) { 84 | // console.log('\x1b[32mcreate\x1b[0m: ' + path); 85 | } 86 | 87 | // Warning messsage 88 | function warning(msg) { 89 | console.error('\x1b[31mwarning\x1b[0m: ' + msg); 90 | } 91 | 92 | function fail(msg) { 93 | console.error('\x1b[31merror\x1b[0m: ' + msg); 94 | process.exit(-1); 95 | } 96 | 97 | // Make directory if NOT exist 98 | function mkDir(dstPath) { 99 | try { 100 | fs.mkdirSync(dstPath, fs.statSync(__dirname).mode); 101 | create(dstPath) 102 | } 103 | catch (e) { 104 | // File exists 105 | if (e.errno = 17) { 106 | warning(e.message); 107 | } 108 | else { 109 | throw e; 110 | } 111 | } 112 | } 113 | 114 | // Copy directory 115 | function copyDir(srcPath, dstPath) { 116 | 117 | mkDir(dstPath); 118 | var files = fs.readdirSync(srcPath); 119 | 120 | for(var i = 0; i < files.length; i++) { 121 | 122 | // Ignore ".*" 123 | if (/^\./.test(files[i])) { 124 | continue; 125 | } 126 | 127 | var srcFile = path.join(srcPath, files[i]); 128 | var dstFile = path.join(dstPath, files[i]); 129 | 130 | var srcStat = fs.statSync(srcFile); 131 | 132 | // Recursive call If direcotory 133 | if (srcStat.isDirectory()) { 134 | copyDir(srcFile, dstFile); 135 | } 136 | // Copy to dstPath if file 137 | else if (srcStat.isFile()) { 138 | // NOT overwrite file 139 | try { 140 | var dstStat = fs.statSync(dstFile); 141 | // File exists 142 | warning("EEXIST, File exists '" + dstFile + "'"); 143 | } 144 | catch (e) { 145 | // File NOT exists 146 | if (e.errno = 2) { 147 | var data = fs.readFileSync(srcFile); 148 | fs.writeFileSync(dstFile, data); 149 | create(dstFile) 150 | } 151 | else { 152 | throw e; 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | function setupHostname(confPath) { 160 | var hostname = os.hostname() + "\n"; 161 | 162 | try { 163 | var fd = fs.openSync(path.join(confPath, 'me'), 'w'); 164 | fs.writeSync(fd, hostname, null); 165 | } 166 | catch (e) { 167 | warning("Unable to write config/me file: " + e); 168 | } 169 | 170 | try { 171 | var fd = fs.openSync(path.join(confPath, 'host_list'), 'w'); 172 | fs.writeSync(fd, hostname, null); 173 | } 174 | catch (e) { 175 | warning("Unable to write config/host_list file: " + e); 176 | } 177 | } 178 | 179 | var readme = [ 180 | "Haraka", 181 | "", 182 | "Congratulations on creating a new installation of Haraka.", 183 | "", 184 | "This directory contains two key directories for how Haraka will function:", 185 | "", 186 | " - config", 187 | " This directory contains configuration files for Haraka. The", 188 | " directory contains the default configuration. You probably want", 189 | " to modify some files in here, particularly `smtp.ini`.", 190 | " - plugins", 191 | " This directory contains custom plugins which you write to run in", 192 | " Haraka. The plugins which ship with Haraka are still available", 193 | " to use.", 194 | " - docs/plugins", 195 | " This directory contains documentation for your plugins.", 196 | "", 197 | "Documentation for Haraka is available via `haraka -h where the name", 198 | "is either the name of a plugin (without the .js extension) or the name of", 199 | "a core Haraka module, such as `Connection` or `Transaction`.", 200 | "", 201 | "To get documentation on writing a plugin type `haraka -h Plugins`.", 202 | "", 203 | ].join("\n"); 204 | 205 | var packageJson = JSON.stringify({ 206 | "name": "haraka_local", 207 | "description": "An SMTP Server", 208 | "version": "0.0.1", 209 | "dependencies": {} 210 | }); 211 | 212 | var plugin_src = [ 213 | "// %plugin%", 214 | "", 215 | "// documentation via: haraka -c %config% -h plugins/%plugin%", 216 | "", 217 | "// Put your plugin code here", 218 | "// type: `haraka -h Plugins` for documentation on how to create a plugin", 219 | "", 220 | ].join("\n"); 221 | 222 | var plugin_doc = [ 223 | "%plugin%", 224 | "========", 225 | "", 226 | "Describe what your plugin does here.", 227 | "", 228 | "Configuration", 229 | "-------------", 230 | "", 231 | "* `config/some_file` - describe what effect this config file has", 232 | "", 233 | ].join("\n"); 234 | 235 | function createFile(filePath, data, info) { 236 | try { 237 | if (path.existsSync(filePath) && !parsed.force) { 238 | throw filePath + " already exists"; 239 | } 240 | var fd = fs.openSync(filePath, 'w'); 241 | var output = data.replace(/\%(\w+)\%/g, function (i, m1) { return info[m1] }); 242 | fs.writeSync(fd, output, null); 243 | } 244 | catch (e) { 245 | warning("Unable to create file: " + e); 246 | } 247 | } 248 | 249 | if (parsed.version) { 250 | console.log("\033[32;40mHaraka.js\033[0m — Version: " + ver); 251 | } 252 | else if (parsed.list) { 253 | console.log("\033[32;40m*global\033[0m\n" + listPlugins()); 254 | if (parsed['configs']) { 255 | console.log("\033[32;40m*local\033[0m\n" + listPlugins(parsed.configs)); 256 | } 257 | } 258 | else if (parsed.help) { 259 | if (parsed.help === 'true') { 260 | console.log(usage); 261 | } 262 | else { 263 | var md_path, 264 | md_paths = [ 265 | path.join(base, 'docs', parsed.help + '.md'), 266 | path.join(base, 'docs', 'plugins', parsed.help + '.md'), 267 | ]; 268 | if (parsed.configs) { 269 | md_paths.unshift(path.join(parsed.configs, 'docs', 'plugins', parsed.help + '.md')); 270 | md_paths.unshift(path.join(parsed.configs, 'docs', parsed.help + '.md')); 271 | } 272 | for (var i=0, j=md_paths.length; i([message], [code]); 47 | 48 | The function name is required and maps to the list of defined status codes 49 | in RFC 3463. All of the available functions are detailed in the table below. 50 | 51 | [message] is optional and should contain the message that you would like to be 52 | returned to the client, this value can be a string or an array which can 53 | contain multiple elements which will cause a multi-line reply to be sent to the client. 54 | If a message is not supplied, then the default message for the DSN function 55 | will be used. 56 | 57 | [code] is optional and should be a numeric SMTP status code that should be 58 | returned to the client. 59 | 60 | 61 | ###Available DSN functions 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 |
FunctionDefault SMTP Status CodeEnhanced Status CodeDefault Message
Class: Other or Undefined Status X.0.0
unspecified450X.0.0Other undefined status
Class: Addressing Status X.1.X
addr_unspecified450X.1.0Other address status
addr_bad_dest_mailbox550X.1.1Bad destination mailbox address
addr_bad_dest_system550X.1.2Bad destination system address
addr_bad_dest_syntax550X.1.3Bad destination mailbox address syntax
addr_dest_ambigous450X.1.4Destination mailbox address ambiguous
addr_rcpt_ok220X.1.5Destination address valid
addr_mbox_mobed550X.1.6Destination mailbox has moved, No forwarding address
addr_bad_from_syntax550X.1.7Bad sender"s mailbox address syntax
addr_bad_from_system550X.1.8Bad sender"s system address
Class: Mailbox Status X.2.X
mbox_unspecified450X.2.0Other or undefined mailbox status
mbox_disabled550X.2.1Mailbox disabled, not accepting messages
mbox_full450X.2.2Mailbox full
mbox_msg_too_long550X.2.3Message length exceeds administrative limit
mbox_list_expansion_problem450X.2.4Mailing list expansion problem
Class: Mail System Status X.3.X
sys_unspecified450X.3.0Other or undefined mail system status
sys_disk_full450X.3.1Mail system full
sys_not_accepting_mail450X.3.2System not accepting network messages
sys_not_supported450X.3.3System not capable of selected features
sys_msg_too_big550X.3.4Message too big for system
sys_incorrectly_configured450X.3.5System incorrectly configured
Class: Network and Routing Status X.4.X
net_unspecified450X.4.0Other or undefined network or routing status
net_no_answer450X.4.1No answer from host
net_bad_connection450X.4.2Bad connection
net_directory_server_failed450X.4.3Directory server failure
net_unable_to_route550X.4.4Unable to route
net_system_congested450X.4.5Mail system congestion
net_routing_loop550X.4.6Routing loop detected
net_delivery_time_expired550X.4.7Delivery time expired
Class: Mail Delivery Protocol Status X.5.X
proto_unspecified450X.5.0Other or undefined protocol status
proto_invalid_command550X.5.1Invalid command
proto_syntax_error550X.5.2Syntax error
proto_too_many_recipients450X.5.3Too many recipients
proto_invalid_cmd_args550X.5.4Invalid command arguments
proto_wrong_version450X.5.5Wrong protocol version
Class: Message Content or Media Status X.6.X
media_unspecified450X.6.0Other or undefined media error
media_unsupported550X.6.1Media not supported
media_conv_prohibited550X.6.2Conversion required and prohibited
media_conv_unsupported450X.6.3Conversion required but not supported
media_conv_lossy450X.6.4Conversion with loss performed
media_conv_failed450X.6.5Conversion failed
Class: Security or Policy Status X.7.X
sec_unspecified450X.7.0Other or undefined security status
sec_unauthorized550X.7.1Delivery not authorized, message refused
sec_list_expn_prohibited550X.7.2Mailing list expansion prohibited
sec_conv_failed550X.7.3Security conversion required but not possible
sec_feature_unsupported550X.7.4Security features not supported
sec_crypto_failure550X.7.5Cryptographic failure
sec_crypto_algo_unsupported450X.7.6Cryptographic algorithm not supported
sec_msg_integrity_failure550X.7.7Message integrity failure
Convenience functions
no_such_user550X.1.1No such user
temp_resolver_failed450X.4.3Temporary address resolution failure
too_many_hops550X.4.6Too many hops
bad_sender_ip550X.7.1Bad sender IP
relaying_denied550X.7.1Relaying denied
422 | --------------------------------------------------------------------------------