Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Modern outbound SMTP relay (MTA/MSA) built on Node.js and LevelDB.
This is a labs project meaning that ZoneMTA is not tested in production. In the future it should replace our outbound Postfix servers but so far no actual mail servers have been deployed with ZoneMTA. Handle with care!
_____ _____ _____ _____
|__ |___ ___ ___| |_ _| _ |
| __| . | | -_| | | | | | | |
|_____|___|_|_|___|_|_|_| |_| |__|__|
The goal of this project is to provide granular control over routing different messages. Trusted senders can be routed through high-speed (more connections) virtual "sending zones" that use high reputation IP addresses, less trusted senders can be routed through slower (less connections) virtual "sending zones" or through IP addresses with less reputation. In addition the server comes packed with features more common to commercial software, ie. message rewriting or HTTP API for posting messages.
ZoneMTA is comparable to Haraka but unlike Haraka it's for outbound only. Both systems run on Node.js and have a built in plugin system even though the designs are somewhat different. The plugin system (and a lot more as well) for ZoneMTA is inherited from the Nodemailer project and thus do not have direct relations to Haraka.
Assuming Node.js (v6.0.0+), build tools and git. There must be nothing listening on ports 2525 (SMTP), 8080 (HTTP API) and 8081 (internal data channel). All these ports are configurable.
Run as any user (does not need to be root):
$ npm install -g zone-mta
$ zone-mta create path/to/app
$ zone-mta run -d path/to/app
If everything succeeds then you should have a SMTP relay with no authentication running on localhost port 2525 (does not accept remote connections).
See default.js for all possible config options that you can use for config.json in your app folder.
Messages are dropped for delivery either by SMTP or HTTP API. Message is processed as a stream, so it shouldn't matter if the message is very large in size (except if a very large message is submitted using the JSON API). This applies also to DKIM body hash calculation – the hash is calculated chunk by chunk as the message stream flows through (actual signature is generated out of the body hash when delivering the message to destination). The incoming stream starts from incoming connection and ends in LevelDB, so if there's an error in any step between these two, the error is reported back to the client and the message is rejected. If impartial data is stored to LevelDB it gets garbage collected after some time (all message bodies without referencing delivery rows are deleted automatically)
Delivering messages to destination
Check the WIKI for more details
npm install --global --production windows-build-tools
to get these toolsnpm install --production
node app.js
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.You can run the server using any user account. If you want to bind to a low port (eg. 587) you need to start out as root. Once the port is bound the user is downgraded to some other user defined in the config file (root privileges are not required once the server has started).
NB! The user the server process runs as must have write permissions for the LevelDB queue folder
Default configuration can be found from default.js. Instead of changing values in that file you should create another config file that uses the name from NODE_ENV environment variable (defaults to 'development'). So for local development you should have a file called 'development.js' and in production 'production.js' in the same config folder. Values in these files override only the touched keys keeping everything else as by default.
For example if the default.js states an object with multiple properties like this:
{
mailerDaemon: {
name: 'Mail Delivery Subsystem',
address: 'mailer-daemon@' + os.hostname()
}
}
Then you can override only a single property without changing the other values like this in development.js:
{
mailerDaemon: {
name: 'Override default value'
}
}
cp ./setup/zone-mta.service /etc/systemd/system/
systemctl enable zone-mta.service
service zone-mta start
All communication between ZoneMTA and your actual configuration server is handled over HTTP. For example when a user needs to be authenticated then ZoneMTA makes a HTTP request with user info to a provided HTTP address. If ZoneMTA wants to notify about a bounced message then a HTTP request is made. If ZoneMTA wants to check if an user can send messages another HTTP request is made etc. It also means that ZoneMTA does not have a built in user database, this is left entirely to your own application.
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.
Using LeveldDB means that you do not run out of inodes when you have a large queue, you can pile up even millions of messages (assuming you do not run out of disk space first)
DKIM signing support is built in to ZoneMTA. If a new mail transaction is initiated a HTTP call is made against configuration server with the transaction info (includes MAIL FROM address, authenticated username and connection info). If the configuration server responds with DKIM keys (multiple keys allowed) then these keys are used to sign the outgoing message. See more here
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 (IP addresses are not locked, you can assign the same IP for multiple Zones or multiple times for a single Zone). You can also specify the amount of max parallel outgoing connections for a Sending Zone.
To preselect a Zone to be used for a specific message you can use the X-Sending-Zone
header key
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.
You can define specific header values in the Sending Zone configuration with the routingHeaders
option. For example if you want to send messages that contain the header 'X-User-ID' with value '123' then you can configure it like this:
'sending-zone': {
...
routingHeaders: {
'x-user-id': '123'
}
}
You also define that all senders with a specific From domain name are routed through a specific domain. Use senderDomains
option in the Zone config.
'sending-zone': {
...
senderDomains: ['example.com']
}
You also define that all recipients with a specific domain name are routed through a specific domain. Use recipientDomains
option in the Zone config.
'sending-zone': {
...
recipientDomains: ['gmail.com', 'kreata.ee']
}
The routing priority is the following:
X-Sending-Zone
headerroutingHeaders
headerssenderDomains
recipientDomains
If no routing can be detected, then the "default" zone is used.
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.
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. See more here
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.
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.
If the message hard bounces (or after too many retries for soft bounces) a bounce notification is POSTed to an URL. You can also define that a bounce response is sent to the sender email address. See more here
ZoneMTA is an at-least-once delivery system, so messages are deleted from the queue only after positive response from the receiving MX server. If a child starts processing a message the child locks the message and the lock is released automatically if the child dies or master dies. Once normal operations are resumed, the same message can be fetched from the queue again.
Child processes that handle actual delivery keep a TCP connection up against the master process. This connection is used as the data channel for exchanging information about deliveries. If the connection drops for any reason, all current operations are cancelled by the child and non-delivered messages are re-queued by the master. This behavior should limit the possibility of multiple deliveries of the same message. Multiple deliveries can still happen if the process or connection dies exactly on the moment when the MX server acknowledges the message and the notification does not get propagated to the master. This risk of multiple deliveries is preferred over losing messages completely.
Messages might get lost if the database gets into a corrupted state and it is not possible to recover data from it.
You can post a JSON structure to a HTTP endpoint (if enabled) and it will be converted into a rfc822 formatted message and delivered to destination. The JSON structure follows Nodemailer email config (see here) except that file and url access is disabled – you can't define an attachment that loads its contents from a file path or from an url, you need to provide the file contents as base64 encoded string.
You can provide the authenticated username with X-Authenticated-User
header and originating IP with X-Originating-IP
header, both values are optional.
curl -H "Content-Type: application/json" -H "X-Authenticated-User: andris" -H "X-Originating-IP: 123.123.123.123" -X POST http://localhost:8080/send -d '{
"from": "sender@example.com",
"to": "recipient1@example.com, recipient2@example.com",
"subject": "hello",
"text": "hello world!"
}'
You can check the current state of a sending zone (for example "default") with the following query
curl http://localhost:8080/queue/default
The response includes counters about queued and deferred messages and also splits the counter by recipient domains
{
"time": "2016-10-03T13:12:18.128Z",
"started": "2016-10-03T13:12:17.336Z",
"processed": 1,
"queued": 1,
"deferred": 1,
"domains": [{
"domain": "example.com",
"queued": 1,
"deferred": 1
}]
}
If you know the queue id (for example 1578a823de00009fbb) then you can check the current status with the following query
curl http://localhost:8080/message/1578a823de00009fbb
The response includes general information about the message and lists all recipients that are current queued (about to be sent) or deferred (are scheduled to send in the future). This does not include messages already sent or bounced.
{
"meta": {
"id": "1578a823de00009fbb",
"interface": "feeder",
"from": "sender@example.com",
"to": ["recipient1@example.com", "recipient2@example.com"],
"origin": "127.0.0.1",
"originhost": "[127.0.0.1]",
"transhost": "foo",
"transtype": "ESMTP",
"time": 1475497588281,
"dkim": {
"hashAlgo": "sha256",
"bodyHash": "HAuESLcsVfL2FGQCUtFOwTL6Ax18XDXZO2vOeAz+DpI="
},
"headers": [{
"key": "date",
"line": "Date: Mon, 03 Oct 2016 12:26:32 +0000"
}, {
"key": "from",
"line": "From: Sender <sender@example.com>"
}, {
"key": "message-id",
"line": "Message-ID: <95dc84ae-ff9e-4e95-aa75-8ee707bc018d@example.com>"
}, {
"key": "subject",
"line": "subject: test"
}],
"messageId": "<95dc84ae-ff9e-4e95-aa75-8ee707bc018d@example.com>",
"date": "Mon, 03 Oct 2016 12:26:32 +0000",
"parsedEnvelope": {
"from": "sender@example.com",
"to": [],
"cc": [],
"bcc": [],
"replyTo": false,
"sender": false
},
"bodySize": 3458,
"created": 1475497593204
},
"messages": [{
"id": "1578a823de00009fbb",
"seq": "002",
"zone": "default",
"recipient": "recipient1@example.com",
"status": "DEFERRED",
"deferred": {
"first": 1475499253068,
"count": 2,
"last": 1475499774161,
"next": 1475501274161,
"response": "450 4.3.2 Service currently unavailable"
}
}]
}
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.
It should be possible to administer queues using an easy to use web interface.
RocksDB has much better performance both for reading and writing but it's more difficult to set up
In production you probably would want to allow Node.js to use more memory, so you should probably start the app with –max-old-space-size
option
node –max-old-space-size=8192 app.js
This is mostly needed if you want to allow large SMTP envelopes on submission (eg. someone wants to send mail to 10 000 recipients at once) as all recipient data is gathered in memory and copied around before storing to the queue.
ZoneMTA uses LevelDB as the storage backend. While extremely capable and fast there is a small chance that LevelDB gets into a corrupted state. There are options to recover from such state automatically but this usually means dropping a lot of data, so no automatic attempt is made to "fix" the corrupt database by the application. What you probably want to do in such situation would be to move the queue folder to some other location for manual recovery and let ZoneMTA to start over with a fresh and empty queue folder.
If LevelDB is in a corrupt state then no messages are accepted for delivery. A positive response is sent to the client only after the entire contents of the message to send are processed and successfully stored to disk.
European Union Public License 1.1 (details)
In general, EUPLv1.1 is compatible with GPLv2, so it's a copyleft license. Unlike GPL the EUPL license has legally binding translations in every official language of the European Union, including the Estonian language. This is why it was preferred over GPL.
FAQs
Tiny outbound MTA
We found that zone-mta demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.