Opinionated email server
Find a file
2017-07-19 11:22:07 +03:00
config changed API 2017-07-17 16:32:31 +03:00
examples Changed configuration manager 2017-07-16 14:37:33 +03:00
imap-core Added API endpoint to push changes in user account 2017-07-18 17:38:05 +03:00
lib Added user stream demo 2017-07-19 11:06:47 +03:00
public Added user stream demo 2017-07-19 11:06:47 +03:00
.eslintrc updated indexes 2017-07-10 21:55:54 +03:00
.gitignore Changed configuration manager 2017-07-16 14:37:33 +03:00
api.js Added user stream demo 2017-07-19 11:06:47 +03:00
Gruntfile.js Use prettier for formatting 2017-06-03 09:51:58 +03:00
imap.js changed API 2017-07-17 16:32:31 +03:00
indexes.yaml Added user stream demo 2017-07-19 11:06:47 +03:00
LICENSE test 2017-05-23 19:42:53 +03:00
lmtp.js Changed configuration manager 2017-07-16 14:37:33 +03:00
logger.js Changed configuration manager 2017-07-16 14:37:33 +03:00
logo.txt Added user field to indexes and message queries to enable sharding 2017-07-13 17:04:41 +03:00
package.json Changed configuration manager 2017-07-16 14:37:33 +03:00
pop3.js Added API endpoint to push changes in user account 2017-07-18 17:38:05 +03:00
README.md Updated README 2017-07-19 11:22:07 +03:00
server.js Changed configuration manager 2017-07-16 14:37:33 +03:00
worker.js Changed configuration manager 2017-07-16 14:37:33 +03:00

Agent Wild Duck

Wild Duck is a distributed IMAP/POP3 server built with Node.js, MongoDB and Redis. Node.js runs the application, MongoDB is used as the mail store and Redis is used for ephemeral actions like publish/subscribe, locking and caching.

NB! Wild Duck is currently in beta. Use it on your own responsibility.

Distributed means that Wild Duck uses a distributed database (sharded+replicated MongoDB) as a backend for storing all data, including emails. Wild Duck instances are stateless, you can have multiple IMAP server instances running and a client can connect to any of these. Wild Duck uses a write ahead log to keep IMAP sessions in sync.

Usage

Assuming you have MongoDB and Redis running somewhere.

Step 1. Get the code from github

$ git clone git://github.com/wildduck-email/wildduck.git
$ cd wildduck

Step 2. Install dependencies

Install dependencies from npm

$ npm install --production

Step 3. Run the server

To use the default config file, run the following:

node server.js

Or if you want to override default configuration options with your own, run the following (custom config file is merged with the default, so specify only these values that you want to change):

node server.js --config=/etc/wildduck.toml

For additional config options, see the wild-config documentation.

Step 4. Create an user account

See see below for details about creating new user accounts

Step 5. Use an IMAP/POP3 client to log in

Any IMAP or POP3 client will do. Use the credentials from step 4. to log in.

Goals of the Project

  1. Build a scalable and distributed IMAP/POP3 server that uses clustered database instead of single machine file system as mail store
  2. Allow using internationalized email addresses
  3. Provide Gmail-like features like pushing sent messages automatically to Sent Mail folder or notifying about messages moved to Junk folder so these could be marked as spam
  4. Provide parsed mailbox and message data over HTTP. This should make creating webmail interfaces super easy, no need to parse RFC822 messages to get text content or attachments

FAQ

Does it work?

Yes, it does. You can run the server and get working IMAP and POP3 servers for mail store, LMTP server for pushing messages to the mail store and HTTP API server to create new users. All handled by Node.js, MongoDB and Redis, no additional dependencies needed. Provided services can be disabled and enabled one by one so, for example you could process just IMAP in one host and LMTP in another.

What are the killer features?

  1. Stateless. Start as many instances as you want. You can start multiple Wild Duck instances in different machines and as long as they share the same MongoDB and Redis settings, users can connect to any instances. This is very different from the traditional IMAP servers where a single user always needs to connect (or be proxied) to the same IMAP server. Wild Duck keeps all required state information in MongoDB, so it does not matter which IMAP instance you use.
  2. Centralized authentication which allows modern features like 2FA, application specific passwords, authentication scopes, revoking authentication tokens, audit logging and even profile files to auto configure Apple email clients without providing master password
  3. Works on any OS including Windows. At least if you get MongoDB and Redis running first.
  4. Focus on internationalization, ie. supporting email addresses with non-ascii characters
  5. De-duplication of attachments. If the same attachment is referenced by different messages then only a single copy of the attachment is stored. Attachment is stored in the encoded form (eg. encoded in base64) to not break any signatures so the resulting encoding must match as well.
  6. Access messages both using IMAP and HTTP API. The latter serves parsed data, so no need to fetch RFC822 messages and parse out html, plaintext content or attachments. It is super easy to create a webmail interface on top of this.
  7. Build in address labels: username+label@example.com is delivered to username@example.com
  8. Super easy to tweak. The entire codebase is pure JavaScript, so there's nothing to compile or anything platform specific. If you need to tweak something then change the code, restart the app and you're ready to go. If it works on one machine then most probably it works in every other machine as well.

Isn't it bad to use a database as a mail store?

Yes, historically it has been considered a bad practice to store emails in a database. And for a good reason. The data model of relational databases like MySQL does not work well with tree like structures (email mime tree) or large blobs (email source).

Notice the word "relational"? In fact document stores like MongoDB work very well with emails. Document store is great for storing tree-like structures and while GridFS is not as good as "real" object storage, it is good enough for storing the raw parts of the message. Additionally there's nothing too GridFS specific, so (at least in theory) it could be replaced with any object store.

Here's a list of alternative email/IMAP servers that also use a database for storing email messages:

How does it work?

Whenever a message is received Wild Duck parses it into a tree-like structure based on the MIME tree and stores this tree to MongoDB. Attachments are removed from the tree and stored separately in GridStore. If a message needs to be loaded then Wild Duck fetches the tree structure first and, if needed, loads attachments from GridStore and then compiles it back into the original RFC822 message. The result should be identical to the original messages unless the original message used unix newlines, these might be partially replaced with windows newlines.

Wild Duck tries to keep minimal state for sessions (basically just a list of currently known UIDs and latest MODSEQ value) to be able to distribute sessions between different hosts. Whenever a mailbox is opened the entire message list is loaded as an array of UID values. The first UID in the array element points to the message nr. 1 in IMAP, second one points to message nr. 2 etc.

Actual update data (information about new and deleted messages, flag updates and such) is stored to a journal log and an update beacon is propagated through Redis pub/sub whenever something happens. If a session detects that there have been some changes in the current mailbox and it is possible to notify the user about it (eg. a NOOP call was made), journaled log is loaded from the database and applied to the UID array one action at a time. Once all journaled updates have applied then the result should match the latest state. If it is not possible to notify the user (eg a FETCH call was made), then journal log is not loaded and the user continues to see the old state.

E-Mail Protocol support

Wild Duck IMAP server supports the following IMAP standards:

  • The entire IMAP4rev1 suite with some minor differences from the spec. See below for IMAP Protocol Differences for a complete list
  • IDLE (RFC2177) notfies about new and deleted messages and also about flag updates
  • CONDSTORE (RFC4551) and ENABLE (RFC5161) supports most of the spec, except metadata stuff which is ignored
  • STARTTLS (RFC2595)
  • NAMESPACE (RFC2342) minimal support, just lists the single user namespace with hierarchy separator
  • UNSELECT (RFC3691)
  • UIDPLUS (RFC4315)
  • SPECIAL-USE (RFC6154)
  • ID (RFC2971)
  • MOVE (RFC6851)
  • AUTHENTICATE PLAIN (RFC4959) and SASL-IR
  • APPENDLIMIT (RFC7889) maximum global allowed message size is advertised in CAPABILITY listing
  • UTF8=ACCEPT (RFC6855) this also means that Wild Duck natively supports unicode email usernames. For example андрис@уайлддак.орг is a valid email address that is hosted by a test instance of Wild Duck
  • QUOTA (RFC2087) Quota size is global for an account, using a single quota root. Be aware that quota size does not mean actual byte storage in disk, it is calculated as the sum of the RFC822 sources of stored messages. Actual disk usage is larger as there are database overhead per every message.
  • COMPRESS=DEFLATE (RFC4978) Compress traffic between the client and the server

Wild Duck more or less passes the ImapTest Stress Testing run. Common errors that arise in the test are unknown labels (Wild Duck doesn't send unsolicited FLAGS updates even though it does send unsolicited FETCH FLAGS updates) and sometimes NO for STORE (messages deleted in one session can not be updated in another).

For testing I did a test run of 3 million sessions. Here's the test script:

while true
do
  imaptest host=127.0.0.1 port=143 \
           user="user%d" pass="test" \
           mbox="dovecot-crlf" rawlog \
           clients=30 msgs=100 copybox="Junk" \
           own_msgs own_flags random
done

POP3 Support

In addition to the required POP3 commands (RFC1939) Wild Duck supports the following extensions:

  • UIDL
  • USER
  • PASS
  • SASL PLAIN
  • PIPELINING
  • TOP

POP3 command behaviors

All changes to messages like deleting messages or marking messages as seen are stored in storage only in the UPDATE stage (eg. after calling QUIT). Until then the changes are preserved in memory only. This also means that if a message is downloaded but QUIT is not issued then the message does not get marked as Seen.

LIST

POP3 listing displays the newest 250 messages in INBOX (configurable)

UIDL

Wild Duck uses message _id value (24 byte hex) as the unique ID. If a message is moved from one mailbox to another then it might re-appear in the listing.

RETR

If a messages is downloaded by a client this message gets marked as Seen

DELE

If a messages is deleted by a client this message gets marked as Seen and moved to Trash folder

HTTP API

NB! The HTTP API is being re-designed

Users, mailboxes and messages can be managed with HTTP requests against Wild Duck API

TODO:

  1. Expose counters (seen/unseen messages, message count in mailbox etc.)
  2. Search/list messages
  3. Expose journal updates through WebSocket or similar

Responses

All failed responses look like the following:

{
    "error": "Some error message"
}

Users

User accounts

Get one user

GET /users/{user}

Returns data about a specific user

Parameters

  • user is the ID of the user

Example

curl "http://localhost:8080/users/59467f27535f8f0f067ba8e6"

Response for a successful operation:

{
  "success": true,
  "id": "59467f27535f8f0f067ba8e6",
  "username": "testuser",
  "address": "testuser@example.com",
  "retention": false,
  "limits": {
    "quota": {
      "allowed": 1024,
      "used": 128
    },
    "recipients": {
      "allowed": 1024,
      "used": 15,
      "ttl": false
    },
    "forwards": {
      "allowed": 2000,
      "used": 8,
      "ttl": false
    }
  }
}

Recipient/forward limits assume that messages are sent using ZoneMTA with zonemta-wildduck plugin, otherwise the counters are not updated.

Add a new user

POST /users

Creates a new user, returns the ID upon success.

Parameters

  • username (required) is the username of the user. This is not an email address but authentication username, use only letters and numbers
  • password (required) is the password for the user
  • address is the main email address for the user. If address is not set then a new one is generated based on the username and current domain name
  • quota is the maximum storage in bytes allowed for this user. If not set then the default value is used
  • retention is the default retention time in ms for mailboxes. Messages in Trash and Junk folders have a capped retention time of 30 days.
  • language is the language code for the user, eg. "en" or "et". Mailbox names for the default mailboxes (eg. "Trash") depend on the language
  • recipients is the maximum number of recipients allowed to send mail to in a 24h window. Requires ZoneMTA with the Wild Duck plugin
  • forwards is the maximum number of forwarded emails in a 24h window. Requires ZoneMTA with the Wild Duck plugin

Example

curl -XPOST "http://localhost:8080/users" -H 'content-type: application/json' -d '{
  "username": "testuser",
  "password": "secretpass",
  "address": "testuser@example.com"
}'

Response for a successful operation:

{
  "success": true,
  "id": "59467f27535f8f0f067ba8e6"
}

After you have created an user you can use these credentials to log in to the IMAP server.

Update user details

PUT /users/{user}

Updates the properties of an user. Only specify these fields that you want to be updated.

Parameters

  • user (required) is the ID of the user
  • password is the updated password for the user (do not set if you do not want to change user password)
  • quota is the maximum storage in bytes allowed for this user
  • retention is the default retention time in ms for mailboxes. Messages in Trash and Junk folders have a capped retention time of 30 days.
  • language is the language code for the user, eg. "en" or "et". Mailbox names for the default mailboxes (eg. "Trash") depend on the language
  • recipients is the maximum number of recipients allowed to send mail to in a 24h window. Requires ZoneMTA with the Wild Duck plugin
  • forwards is the maximum number of forwarded emails in a 24h window. Requires ZoneMTA with the Wild Duck plugin

Example

Set user quota to 1 kilobyte:

curl -XPUT "http://localhost:8080/users/59467f27535f8f0f067ba8e6" -H 'content-type: application/json' -d '{
  "quota": 1024
}'

Response for a successful operation:

{
  "success": true
}

UserAddresses

Manage email addresses and aliases for an user.

List addresses

GET /users/{user}/addresses

Lists all registered email addresses for an user.

Parameters

  • user (required) is the ID of the user

Example

curl "http://localhost:8080/users/59467f27535f8f0f067ba8e6/addresses"

Response for a successful operation:

{
  "success": true,
  "addresses": [
    {
      "id": "596c9c37ef2213165daadc6b",
      "address": "testuser@example.com",
      "main": true,
      "created": "2017-07-17T11:15:03.841Z"
    },
    {
      "id": "596c9dd31b201716e764efc2",
      "address": "user@example.com",
      "main": false,
      "created": "2017-07-17T11:21:55.960Z"
    }
  ]
}

Get one address

GET /users/{user}/addresses/{address}

Returns data about a specific address.

Parameters

  • user (required) is the ID of the user
  • address (required) is the ID of the address

Example

curl "http://localhost:8080/users/59467f27535f8f0f067ba8e6/addresses/596c9c37ef2213165daadc6b"

Response for a successful operation:

{
  "success": true,
  "id": "596c9c37ef2213165daadc6b",
  "address": "testuser@example.com",
  "main": true,
  "created": "2017-07-17T11:15:03.841Z"
}

Add a new address

POST /users/{user}/addresses

Creates a new email address alias for an existing user, returns the ID upon success.

Parameters

  • user (required) is the ID of the user
  • address (required) is the email address to use as an alias for this user. You can also use internationalized email addresses like андрис@уайлддак.орг.
  • main indicates that this is the default address for that user (defaults to false)

Example

curl -XPOST "http://localhost:8080/users/59467f27535f8f0f067ba8e6/addresses" -H 'content-type: application/json' -d '{
  "address": "user@example.com"
}'

Response for a successful operation:

{
  "success": true,
  "id": "596c9dd31b201716e764efc2"
}

After you have registered a new address then LMTP maildrop server starts accepting mail for it and stores messages to the users mailbox.

Update address details

PUT /users/{user}/addresses/{address}

Updates the properties of an address. Currently, only main can be updated.

Parameters

  • user (required) is the ID of the user
  • address (required) is the ID of the address
  • main must be true. Indicates that this is the default address for that user

Example

curl -XPUT "http://localhost:8080/users/59467f27535f8f0f067ba8e6/addresses/596c9dd31b201716e764efc2" -H 'content-type: application/json' -d '{
  "main": true
}'

Response for a successful operation:

{
  "success": true
}

Delete an alias address

DELETE /users/{user}/addresses/{address}

Deletes an email address alias from an existing user.

Parameters

  • user (required) is the ID of the user
  • address (required) is the ID of the address

Example

curl -XDELETE "http://localhost:8080/users/59467f27535f8f0f067ba8e6/addresses/596c9dd31b201716e764efc2"

Response for a successful operation:

{
  "success": true
}

UserQuota

Recalculate user quota

POST /users/{user}/quota/reset

Recalculates used storage for an user. Use this when it seems that quota counters for an user do not match with reality.

Parameters

  • user (required) is the ID of the user

Example

curl -XPOST "http://localhost:8080/users/59467f27535f8f0f067ba8e6/quota/reset" -H 'content-type: application/json' -d '{}'

Response for a successful operation:

{
  "success":true,
  "storageUsed": 128
}

Be aware though that this method is not atomic and should be done only if quota counters are way off.

UserUpdates

Get user related events as an Event Source stream

Stream update events

GET /users/{user}/updates

Streams changes in user account as EventSource stream

Parameters

  • user (required) is the ID of the user

Example

curl "http://localhost:8080/users/59467f27535f8f0f067ba8e6/updates"

Response stream:

data: {"command":"EXISTS", "message":"596e0703f0bdd512aeac3600", "mailbox":"596c9c37ef2213165daadc65",...}
id: 596e0703f0bdd512aeac3605

data: {"command":"CREATE","mailbox":"596e09853f845a14f3620b5c","name":"My Mail",...}
id: 596e09853f845a14f3620b5d

First entry in the event stream indicates that a message with id 596e0703f0bdd512aeac3600 was added to mailbox 596c9c37ef2213165daadc65, second entry indicates that a new mailbox called "My Mail" with id 596e09853f845a14f3620b5c was created.

Be aware though that this connection needs to be properly closed if you do not want to end up with memory leaks.

You can see a demo of catching user events when navigating to http://localhost:8080/public/ (assuming localhost:8080 is your API host).

Message filtering

The filtering system is subject to change with the API updates. Most probably the filters are going to reside in separate collection and not as part of the user object.

Wild Duck has built-in message filtering in LMTP server. This is somewhat similar to Sieve even though the filters are not scripts.

Filters are configuration objects stored in the filters array of the users object.

Example filter

{
    // identifier for this filter
    id: ObjectId('abcdefghij...'),

    // query to check messages against
    query: {
        // message must match all filter rules for the filter actions to apply
        // all values are case insensitive
        headers: {
            // partial string match against decoded From: header
            from: 'sender@example.com',
            // partial string match against decoded To: header
            to: 'recipient@example.com',
            // partial string match against decoded Subject: header
            subject: 'Väga tõrges'
        },

        // partial string match (case insensitive) against decoded plaintext message
        text: 'Mõigu ristis oli mis?',

        // positive: must have attachments, negative: no attachments
        ha: 1,

        // positive: larger than size, negative: smaller than abs(size)
        size: 10
    },
    // what to do if the filter query matches the message
    action: {

        // mark message as seen
        unseen: false,

        // mark message as flagged
        flag: true,

        // set mailbox ID
        mailbox: 'aaaaa', // must be ObjectID!

        // positive spam, negative ham
        spam: 1,

        // if true, delete message
        delete: false
    }
}

NB! If you do not care about an action field then do not set it, otherwise matches from other filters do not apply

Sharding

Shard the following collections by these keys:

sh.enableSharding('wildduck');
sh.shardCollection('wildduck.messages', { mailbox: 1, uid: 1 });
sh.shardCollection('wildduck.threads', { user: 'hashed' });
sh.shardCollection('wildduck.attachments.files', { 'metadata.h': 'hashed' });
sh.shardCollection('wildduck.attachments.chunks', { files_id: 'hashed' });

Attachments collections might reside in a different database than default. Modify sharding namespaces accordingly (and do not forget to enable sharding for the attachments database)

IMAP Protocol Differences

This is a list of known differences from the IMAP specification. Listed differences are either intentional or are bugs that became features.

  1. \Recent flags is not implemented and most probably never will be (RFC3501 2.3.2.)
  2. RENAME does not touch subfolders which is against the spec (RFC3501 6.3.5. If the name has inferior hierarchical names, then the inferior hierarchical names MUST also be renamed.). Wild Duck stores all folders using flat hierarchy, the "/" separator is fake and only used for listing mailboxes
  3. Unsolicited FLAGS responses (RFC3501 7.2.6.) and PERMANENTFLAGS are not sent (except for as part of SELECT and EXAMINE responses). Wild Duck notifies about flag updates only with unsolicited FETCH updates.
  4. Wild Duck responds with NO for STORE if matching messages were deleted in another session
  5. CHARSET argument for the SEARCH command is ignored (RFC3501 6.4.4.)
  6. Metadata arguments for SEARCH MODSEQ are ignored (RFC7162 3.1.5.). You can define <entry-name> and <entry-type-req> values but these are not used for anything
  7. SEARCH TEXT and SEARCH BODY both use MongoDB $text index against decoded plaintext version of the message. RFC3501 assumes that it should be a string match either against full message (TEXT) or body section (BODY).
  8. What happens when FETCH is called for messages that were deleted in another session? Not sure, need to check

Any other differences are most probably real bugs and unintentional.

Testing

Create an email account and use your IMAP client to connect to it. To send mail to this account, run the example script:

node examples/push-mail.js username@example.com

This should "deliver" a new message to the INBOX of username@example.com by using the built-in LMTP maildrop interface. If your email client is connected then you should promptly see the new message.

Outbound SMTP

Use ZoneMTA with the ZoneMTA-WildDuck plugin. This gives you an outbound SMTP server that uses Wild Duck accounts for authentication.

Inbound SMTP

Use Haraka with queue/lmtp plugin. Wild Duck specific recipient processing plugin coming soon!

Future considerations

  • Add interoperability with current servers, for example by fetching authentication data from MySQL

  • Optimize FETCH queries to load only partial data for BODY subparts

  • Parse incoming message into the mime tree as a stream. Currently the entire message is buffered in memory before being parsed.

  • CPU usage seems a bit too high, there is probably a ton of profiling to do

  • Maybe allow some kind of message manipulation through plugins?

  • Wild Duck does not plan to be the most feature-rich IMAP client in the world. Most IMAP extensions are useless because there aren't too many clients that are able to benefit from these extensions. There are a few extensions though that would make sense to be added to Wild Duck:

    • IMAP4 non-synchronizing literals, LITERAL- (RFC7888). Synchronized literals are needed for APPEND to check mailbox quota, small values could go with the non-synchronizing version.
    • LIST-STATUS (RFC5819)
    • What else? (definitely not NOTIFY nor QRESYNC)

License

Wild Duck Mail Agent is licensed under the European Union Public License 1.1.