forked from zone-eu/zone-mta
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d401327
Showing
23 changed files
with
4,342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use strict'; | ||
|
||
module.exports = { | ||
rules: { | ||
indent: [2, 4, { | ||
SwitchCase: 1 | ||
}], | ||
quotes: [2, 'single'], | ||
'linebreak-style': [2, 'unix'], | ||
semi: [2, 'always'], | ||
strict: [2, 'global'], | ||
eqeqeq: 2, | ||
'dot-notation': 2, | ||
curly: 2, | ||
'no-fallthrough': 2, | ||
'quote-props': [2, 'as-needed'], | ||
'no-unused-expressions': [2, { | ||
allowShortCircuit: true | ||
}], | ||
'no-unused-vars': 2, | ||
'no-undefined': 2, | ||
'handle-callback-err': 2, | ||
'no-new': 2, | ||
'new-cap': 2, | ||
'no-eval': 2, | ||
'no-invalid-this': 2, | ||
radix: [2, 'always'], | ||
'no-use-before-define': [2, 'nofunc'], | ||
'callback-return': [2, ['callback', 'cb', 'done']], | ||
'comma-dangle': [2, 'never'], | ||
'comma-style': [2, 'last'], | ||
'no-regex-spaces': 2, | ||
'no-empty': 2, | ||
'no-duplicate-case': 2, | ||
'no-empty-character-class': 2, | ||
'no-redeclare': [2, { | ||
builtinGlobals: true | ||
}], | ||
'block-scoped-var': 2, | ||
'no-sequences': 2, | ||
'no-throw-literal': 2, | ||
'no-useless-call': 2, | ||
'no-useless-concat': 2, | ||
'no-void': 2, | ||
yoda: 2, | ||
'no-undef': 2, | ||
'global-require': 2, | ||
'no-var': 2, | ||
'no-bitwise': 2, | ||
'no-lonely-if': 2, | ||
'no-mixed-spaces-and-tabs': 2, | ||
'arrow-body-style': [2, 'as-needed'], | ||
'arrow-parens': [2, 'as-needed'], | ||
'prefer-arrow-callback': 2, | ||
'object-shorthand': 2, | ||
'prefer-spread': 2 | ||
}, | ||
env: { | ||
es6: true, | ||
node: true | ||
}, | ||
extends: 'eslint:recommended', | ||
globals: { | ||
it: true, | ||
describe: true, | ||
beforeEach: true, | ||
afterEach: true | ||
}, | ||
fix: true | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
node_modules | ||
.DS_Store | ||
npm-debug.log | ||
queuedata | ||
appendlog | ||
.lev_history | ||
keys/*.pem | ||
test/queuetest | ||
config/production.* | ||
*v8.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
'use strict'; | ||
|
||
module.exports = function (grunt) { | ||
|
||
// Project configuration. | ||
grunt.initConfig({ | ||
eslint: { | ||
all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'Gruntfile.js', 'app.js'] | ||
}, | ||
|
||
nodeunit: { | ||
all: ['test/**/*-test.js'] | ||
} | ||
}); | ||
|
||
// Load the plugin(s) | ||
grunt.loadNpmTasks('grunt-eslint'); | ||
grunt.loadNpmTasks('grunt-contrib-nodeunit'); | ||
|
||
// Tasks | ||
grunt.registerTask('default', ['eslint', 'nodeunit']); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# ZoneMTA | ||
|
||
Tiny outbound SMTP relay built on Node.js and LevelDB. | ||
|
||
The goal of this project is to provide granular control over routing different messages. Trusted senders can be routed through high-speed "sending zones" that use high reputation IP addresses, less trusted senders can be routed through slower "sending zones" or through IP addresses with less reputation. | ||
|
||
## Features | ||
|
||
- Send large messages with low overhead | ||
- Automatic DKIM signing | ||
- Adds Message-Id and Date headers if missing | ||
- Queue is stored in LevelDB | ||
- Sending Zone support: send different messages using different IP addresses | ||
- Assign specific recipient domains to specific Sending Zones | ||
- Built in IPv6 support | ||
- Uses STARTTLS for outgoing messages by default, so no broken padlock images in Gmail | ||
- Smarter bounce handling | ||
- Throttling per Sending Zone connection | ||
|
||
## Setup | ||
|
||
1. Requirements: Node.js v6+ for running the app + compiler for building leveldb | ||
|
||
2. Open Mailqueue folder and install required dependencies: `npm install` | ||
|
||
3. Modify configuration script (if you want to allow connections outside localhost make sure the feeder host is not bound to 127.0.0.1) | ||
|
||
4. Run the server application: `node app.js` | ||
|
||
5. If everything worked then you should have a relay SMTP server running at localhost:2525 (user "test", password "zone", no TLS) | ||
|
||
6. You can find the stats about queues at `http://hostname:8080/queue/default` where `default` is the default Sending Zone name. For other zones, replace the identifier in the URL. The queue counters are approximate. | ||
|
||
## Install as a service | ||
|
||
1. Move zone-mta folder to /opt/zone-mta | ||
2. Copy Systemd service file: `cp ./setup/zone-mta.service /etc/systemd/system/` | ||
3. Enable Systemd service: `systemctl enable zone-mta.service` | ||
4. Start the service: `service zone-mta start` | ||
5. Send a message to application host port 2525, using username 'test' and password 'zone' | ||
|
||
## Large message support | ||
|
||
All data is processed in chunks without reading the entire message into memory, so it does not matter if the message is 1kB or 1GB in size. | ||
|
||
## LevelDB backend | ||
|
||
Using LeveldDB means that you do not run out of inodes when you have a large queue | ||
|
||
## DKIM signing | ||
|
||
DKIM signing support is built in to ZoneMTA. All you need to do is to provide signing keys to use it. DKIM private keys are stored in _./keys_ as _{DOMAIN}.{SELECTOR}.pem_. | ||
|
||
For example if you want to use a key for "kreata.ee" with selector "test" then this private key should be available at ./keys/kreata.ee.test.pem | ||
|
||
DKIM signature is based on the domain name of the From: address or if there is no From: address then by the domain name of the envelope MAIL FROM:. If a matching key can not be found then the message is not signed. | ||
|
||
## Sending Zone | ||
|
||
You can define as many Sending Zones as you want. Every Sending Zone can have its own local address IP pool that is used to send out messages designated for that Zone. You can also specify the amount of max parallel outgoing connections for a Sending Zone. | ||
|
||
Sending Zone routing is handled by message headers: | ||
|
||
``` | ||
X-Sending-Zone: zone-identifier | ||
``` | ||
|
||
For example if you have a Sending Zone called "zone-identifier" set then messages with such header are routed through this Sending Zone. If there is no Sending Zone with that name then "default" Sending Zone is used instead. | ||
|
||
You can also pre-assign specific domains to specific Sending Zones, in which case the message is routed to the correct Sending Zone without additional routing headers based on either the sender or recipient domain. | ||
|
||
## IPv6 support | ||
|
||
IPv6 is supported by default. You can disable it per Sending Zone if you don't need to or can't send messages over IPv6. | ||
|
||
## HTTP based authentication | ||
|
||
If authentication is required then all clients are authenticated against a HTTP endpoint using Basic access authentication. If the HTTP request succeeds then the user is considered as authenticated. | ||
|
||
## Per-Zone domain connection limits | ||
|
||
You can set connection limits for recipient domains per Sending Zone. For example if you have set max 2 connections to a specific domain then even if your queue processor has free slots and there are a lot of messages queued for that domain it will not create more connections than allowed. | ||
|
||
## Bounce handling | ||
|
||
ZoneMTA tries to guess the reason behind rejecting a message – maybe the message was greylisted or maybe your sending IP is blocked by this recipient. Not every bounce is equal. | ||
|
||
## Error Recovery | ||
|
||
Child processes keep file based logs of delivered messages. Whenever a child process crashes or master process goes down this log is used to identify messages that are successfully delivered but are still in the queue. This behavior should limit the possibility of multiple deliveries of the same message. Multiple deliveries can still happen if the process dies exactly on the moment when the MX server acknowledges the message and the process is starting to write to the log file. This risk of preferred multiple deliveries is preferred over losing messages completely. | ||
|
||
## TODO | ||
|
||
### 1\. Domain based throttling | ||
|
||
Currently it is possible to limit active connections against a domain and you can limit sending speed per connection (eg. 10 messages/min per connection) but you can't limit sending speed per domain. If you have set 3 processes, 5 connections and limit sending with 10 messages / minute then what you actually get is 3 _5_ 10 = 150 messages per minute for a Sending Zone. | ||
|
||
### 2\. Web interface | ||
|
||
It should be possible to administer queues using an easy to use web interface. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
'use strict'; | ||
|
||
// Main application file | ||
// Run as 'node app.js' to start | ||
|
||
const config = require('config'); | ||
const log = require('npmlog'); | ||
const createFeederServer = require('./lib/feeder-server'); | ||
const createAPIServer = require('./lib/api-server'); | ||
const createMailQueue = require('./lib/mail-queue'); | ||
const sendingZone = require('./lib/sending-zone'); | ||
|
||
log.level = config.log.level; | ||
process.title = 'zone-mta: master process'; | ||
|
||
let feederServer = createFeederServer(); | ||
let apiServer = createAPIServer(); | ||
let queue = createMailQueue(config.queue); | ||
|
||
// Starts the queueing MTA | ||
feederServer.start(err => { | ||
if (err) { | ||
log.error('Feeder', 'Could not start feeder MTA server'); | ||
log.error('Feeder', err); | ||
return process.exit(1); | ||
} | ||
log.info('Feeder', 'Feeder MTA server started'); | ||
|
||
// Starts the API HTTP REST server that is used by sending processes to fetch messages from the queue | ||
apiServer.start(err => { | ||
if (err) { | ||
log.error('API', 'Could not start API server'); | ||
log.error('API', err); | ||
return process.exit(2); | ||
} | ||
log.info('API', 'API server started'); | ||
|
||
// downgrade user if needed | ||
if (config.group) { | ||
try { | ||
process.setgid(config.group); | ||
log.info('Service', 'Changed group to "%s" (%s)', config.group, process.getgid()); | ||
} catch (E) { | ||
log.info('Service', 'Failed to change group to "%s" (%s)', config.group, E.message); | ||
} | ||
} | ||
if (config.user) { | ||
try { | ||
process.setuid(config.user); | ||
log.info('Service', 'Changed user to "%s" (%s)', config.user, process.getuid()); | ||
} catch (E) { | ||
log.info('Service', 'Failed to change user to "%s" (%s)', config.user, E.message); | ||
} | ||
} | ||
|
||
// Open LevelDB database and start sender processes | ||
queue.init(err => { | ||
if (err) { | ||
log.error('Queue', 'Could not initialize sending queue'); | ||
log.error('Queue', err); | ||
return process.exit(3); | ||
} | ||
log.info('Queue', 'Sending queue initialized'); | ||
|
||
feederServer.queue = queue; | ||
apiServer.queue = queue; | ||
sendingZone.init(queue); | ||
}); | ||
}); | ||
}); | ||
|
||
let stop = () => { | ||
if (queue.closing) { | ||
log.info('Process', 'Force closing...'); | ||
return process.exit(0); | ||
} | ||
log.info('Process', 'Server closing down...'); | ||
queue.closing = true; | ||
|
||
feederServer.close(() => { | ||
// wait until all connections to the feeder SMTP are closed | ||
log.info('Feeder', 'Service closed'); | ||
apiServer.close(() => { | ||
// wait until all connections to the API HTTP are closed | ||
log.info('API', 'Service closed'); | ||
queue.stop(() => { | ||
// wait until DB is closed | ||
log.info('Queue', 'Service closed'); | ||
return process.exit(0); | ||
}); | ||
}); | ||
}); | ||
|
||
let forceExitTimer = setTimeout(() => { | ||
log.info('Process', 'Timed out, force closing...'); | ||
process.exit(0); | ||
}, 10 * 1000); | ||
forceExitTimer.unref(); | ||
}; | ||
|
||
process.on('SIGINT', () => stop()); | ||
process.on('SIGTERM', () => stop()); |
Oops, something went wrong.