Merge branch 'master' of github.com:nodemailer/wildduck

This commit is contained in:
Andris Reinman 2020-05-08 10:44:24 +03:00
commit dfe1e0944b
134 changed files with 1713 additions and 1101 deletions

422
README.md
View file

@ -7,424 +7,16 @@ including emails.
WildDuck tries to follow Gmail in product design. If there's a decision to be made then usually the answer is to do whatever Gmail has done.
## Contact
## Links
- [Website](https://wildduck.email)
- [Documentation](https://docs.wildduck.email)
- [Installation instructions](https://docs.wildduck.email/#/general/install)
- [API Documentation](https://docs.wildduck.email/api)
## Contact
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/nodemailer/wildduck)
## Requirements
- _MongoDB_ to store all data
- _Redis_ for pubsub and counters
- _Node.js_ at least version 8.0.0
**Optional requirements**
- Redis Sentinel for automatic Redis failover
- Build tools to install optional dependencies that need compiling
WildDuck can be installed on any Node.js compatible platform.
## No-SPOF architecture
Every component of the WildDuck mail server can be replicated which eliminates potential single point of failures.
![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/wd.png)
## Storage
Attachment de-duplication and compression gives up to 56% of storage size reduction.
![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/storage.png)
## Usage
### Scripted install
If you have a blank VPS and a free domain name that you can point to that VPS than you can try out the scripted all-included install
[Installation instructions](./setup)
Install script installs and configures all required dependencies and services, including Let's Encrypt based certs, to run WildDuck as a mail server.
Tested on a 10\$ DigitalOcean Ubuntu 16.04 instance.
![](https://cldup.com/TZoTfxPugm.png)
- Web interface at https://wildduck.email that uses WildDuck API
### Manual install
Assuming you have MongoDB and Redis running somewhere.
### Step 1\. Get the code from github
```
$ git clone git://github.com/nodemailer/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](./config/default.toml) 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](https://github.com/nodemailer/wild-config).
### Step 4\. Create a user account
See [API Docs](https://api.wildduck.email/#api-Users-PostUser) 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.
### Docker Install
The easiest way to setup wildduck with a docker image is given below, for more documentation about configuration options in the docker image, refer to
the [wiki page on the Docker](https://github.com/nodemailer/wildduck/wiki/Docker).
A docker hub image built using the [Dockerfile](./Dockerfile) in the repo is also available
To pull the latest pre-built image of wildduck:
```
docker pull nodemailer/wildduck
```
It is also possible to pull a specific version of wildduck by specifying the version as the image tag.
(example, for version 1.20):
```
docker pull nodemailer/wildduck:1.20
```
To run the docker image using the [default config](./config/default.toml), and `mongodb` and `redis` from the host machine, use:
```
docker run --network=host nodemailer/wildduck
```
## 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
# HTTP API
Users, mailboxes and messages can be managed with HTTP requests against WildDuck API
**[API Docs](https://api.wildduck.email/)**
# 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](https://api.wildduck.email/) 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.
### How is security implemented in WildDuck?
Read about WildDuck security implementation from the [Wiki](https://github.com/nodemailer/wildduck/wiki/Security-implementation)
### What are the killer features?
1. **Stateless.** Start as many instances as you want. You can start multiple WildDuck 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. WildDuck keeps all required state information in MongoDB, so it does not matter which IMAP instance you
use.
2. **Scalable** as WildDuck uses sharded MongoDB cluster for the backend storage. If you're running out of space, add a new shard.
3. **No SPOF.** You can run multiple instances of every required service.
4. **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 master password
5. **Works on any OS including Windows.** At least if you get MongoDB and Redis running first.
6. Focus on **internationalization**, ie. supporting email addresses with non-ascii characters
7. **Deduplication of attachments.** If the same attachment is referenced by different messages then only a single copy of the attachment is stored.
8. Access messages both using **IMAP and [HTTP API](https://api.wildduck.email/)**. 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.
9. Built in **address labels**: _username+label@example.com_ is delivered to _username@example.com_
10. Dots in usernames and addresses are informational only. username@example.com is the same as user.name@example.com
11. **HTTP Event Source** to push modifications in user email account to browser for super snappy webmail clients
12. **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.
13. **Better disk usage**. Attachment deduplication and MongoDB compression yield in about 40% smaller disk usage as the sum of all stored email sizes.
14. **Extra security features** like automatic GPG encryption of all stored messages or authenticating with U2F
15. **Exposed logs.** Users have access to logs concerning their account. This includes security logs (authentication attempts, changes on account) and also
message logs
### Isn't it bad to use a database as a mail store?
Yes, historically it has [been considered a bad practice](http://www.memoryhole.net/~kyle/databaseemail.html) 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 servers that also use a database for storing email messages:
- [DBMail](http://www.dbmail.org/) (MySQL, IMAP)
- [Archiveopteryx](http://archiveopteryx.org/) (PostgreSQL, IMAP)
- [ElasticInbox](http://www.elasticinbox.com/) (Cassandra, POP3)
### How does it work?
Whenever a message is received WildDuck 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 WildDuck 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.
WildDuck 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
WildDuck IMAP server supports the following IMAP standards:
- The entire **IMAP4rev1** suite with some minor differences from the spec. See below for [IMAP Protocol Differences](#imap-protocol-differences) for a complete
list
- **IDLE** ([RFC2177](https://tools.ietf.org/html/rfc2177)) notfies about new and deleted messages and also about flag updates
- **CONDSTORE** ([RFC4551](https://tools.ietf.org/html/rfc4551)) and **ENABLE** ([RFC5161](https://tools.ietf.org/html/rfc5161)) supports most of the spec,
except metadata stuff which is ignored
- **STARTTLS** ([RFC2595](https://tools.ietf.org/html/rfc2595))
- **NAMESPACE** ([RFC2342](https://tools.ietf.org/html/rfc2342)) minimal support, just lists the single user namespace with hierarchy separator
- **UNSELECT** ([RFC3691](https://tools.ietf.org/html/rfc3691))
- **UIDPLUS** ([RFC4315](https://tools.ietf.org/html/rfc4315))
- **SPECIAL-USE** ([RFC6154](https://tools.ietf.org/html/rfc6154))
- **ID** ([RFC2971](https://tools.ietf.org/html/rfc2971))
- **MOVE** ([RFC6851](https://tools.ietf.org/html/rfc6851))
- **AUTHENTICATE PLAIN** ([RFC4959](https://tools.ietf.org/html/rfc4959)) and **SASL-IR**
- **APPENDLIMIT** ([RFC7889](https://tools.ietf.org/html/rfc7889)) maximum global allowed message size is advertised in CAPABILITY listing
- **UTF8=ACCEPT** ([RFC6855](https://tools.ietf.org/html/rfc6855)) this also means that WildDuck natively supports unicode email usernames. For example
[андрис@уайлддак.орг](mailto:андрис@уайлддак.орг) is a valid email address that is hosted by a test instance of WildDuck
- **QUOTA** ([RFC2087](https://tools.ietf.org/html/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](https://tools.ietf.org/html/rfc822) sources of stored messages.
- **COMPRESS=DEFLATE** ([RFC4978](https://tools.ietf.org/html/rfc4978)) Compress traffic between the client and the server
WildDuck more or less passes the [ImapTest](https://www.imapwiki.org/ImapTest/TestFeatures) Stress Testing run. Common errors that arise in the test are
unknown labels (WildDuck 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).
In comparison WildDuck is slower in processing single user than Dovecot. Especially when fetching messages, which is expected as Dovecot is reading directly
from filesystem while WildDuck is recomposing messages from different parts.
Raw read/write speed for a single user is usually not relevant anyway as fetching entire mailbox content is not something that happens often. WildDuck offers
better parallelization through MongoDB sharding, so more users should not mean slower response times. It is also more important to offer fast synchronization
speeds between clients (eg. notifications about new email and such) where WildDuck excels due to the write ahead log and the ability to push this log to
clients.
### POP3 Support
In addition to the required POP3 commands ([RFC1939](https://tools.ietf.org/html/rfc1939)) WildDuck 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
WildDuck 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
## Message filtering
WildDuck has built-in message filtering. This is somewhat similar to Sieve even though the filters are not scripts.
Filters can be managed via the [WildDuck API](https://api.wildduck.email/#api-Filters).
## 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._). WildDuck 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). WildDuck notifies
about flag updates only with unsolicited FETCH updates.
4. WildDuck 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](https://docs.mongodb.com/v3.4/reference/operator/query/text/) 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_
9. **Autoexpunge**, meaning that an EXPUNGE is called on background whenever a messages gets a `\Deleted` flag set. This is not in conflict with IMAP RFCs.
Any other differences are most probably real bugs and unintentional.
## Other Differences
1. Messages retrieved from WildDuck might not be exact copies of messages that were initially stored. This mostly affects base64 encoded attachments and content in multipart mime nodes (eg. text like "This is a multi-part message in MIME format.")
## 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-message.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 [WildDuck MTA](https://github.com/nodemailer/wildduck-mta) (which under the hood is [ZoneMTA](https://github.com/zone-eu/zone-mta) with the
[ZoneMTA-WildDuck](https://github.com/nodemailer/zonemta-wildduck) plugin).
This gives you an outbound SMTP server that uses WildDuck accounts for authentication. The plugin authenticates user credentials and also rewrites headers if
needed (if the header From: address does not match user address or aliases then it is rewritten).
## Inbound SMTP
Use [Haraka](http://haraka.github.io/) with [haraka-plugins-wildduck](https://github.com/nodemailer/haraka-plugin-wildduck) to validate recipient addresses and quota usage against the WildDuck users database and to store/filter messages.
#### Spam detection
Use [Rspamd plugin for Haraka](https://github.com/haraka/haraka-plugin-rspamd) in order to detect spam. WildDuck plugin detects Rspamd output and uses this information to send the message either to Inbox or Junk.
## Future considerations
- 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.
- Maybe allow some kind of message manipulation through plugins
- WildDuck 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 WildDuck:
- IMAP4 non-synchronizing literals, LITERAL- ([RFC7888](https://tools.ietf.org/html/rfc7888)). Synchronized literals are needed for APPEND to check mailbox
quota, small values could go with the non-synchronizing version.
- LIST-STATUS ([RFC5819](https://tools.ietf.org/html/rfc5819))
- _What else?_ (definitely not NOTIFY nor QRESYNC)
## Operating WildDuck
### Logging
WildDuck sends gelf-formatted log messages to a Graylog server. Set `log.gelf.enabled=true` in [config](https://github.com/nodemailer/wildduck/blob/2019fd9db6bce1c3167f08e363ab4225b8c8a296/config/default.toml#L59-L66) to use it. Also make sure that the same Gelf settings are set for _zonemta-wildduck_ and _haraka-plugin-wildduck_ in order to get consistent logs about messages throughout the system.
> Graylog logging replaces previously used 'messagelog' database collection
### Import from maildir
There is a tool to import emails from an existing maildir to WildDuck email database. See the tool [here](https://github.com/nodemailer/import-maildir)
### Sharding
WildDuck supports MongoDB sharding. Consider using sharding only if you know that your data storage is large enough to outgrow single replica. Some actions
require scattered queries to be made that might be a hit on performance on a large cluster but most queries include the shard key by default.
Shard the following collections by these keys (assuming you keep attachments in a separate database):
```javascript
sh.enableSharding('wildduck');
// consider using mailbox:hashed for messages only with large shard chunk size
sh.shardCollection('wildduck.messages', { mailbox: 1, uid: 1 });
sh.shardCollection('wildduck.archived', { user: 1, _id: 1 });
sh.shardCollection('wildduck.threads', { user: 'hashed' });
sh.shardCollection('wildduck.authlog', { user: 'hashed' });
sh.enableSharding('attachments');
// attachment _id is a sha256 hash of attachment contents
sh.shardCollection('attachments.attachments.files', { _id: 'hashed' });
sh.shardCollection('attachments.attachments.chunks', { files_id: 'hashed' });
// storage _id is an ObjectID
sh.shardCollection('attachments.storage.files', { _id: 'hashed' });
sh.shardCollection('attachments.storage.chunks', { files_id: 'hashed' });
```
### Disk usage
Tests show that the ratio of attachment contents vs other stuff is around 1:10. This means that you can split your database between multiple disks by using
smaller SSD (eg. 150GB) for message data and indexes and a larger and cheaper SATA (eg. 1TB) for attachment contents. This assumes that you use WiredTiger with
`storage.directoryPerDB:true` and `storage.wiredTiger.engineConfig.directoryForIndexes:true`
Assuming that you use a database named `attachments` for attachment contents:
SSD mount : /var/lib/mongodb
SATA mount: /var/lib/mongodb/attachments/collection
MongoDB does not complain about existing folders so you can prepare the mount before even installing MongoDB.
### Redis Sentinel
WildDuck is able to use Redis Sentinel instead of single Redis master for automatic failover. When using Sentinel and the Redis master fails then it might take
a moment until new master is elected. Pending requests are cached during that window, so most operations should succeed eventually. You might want to test
failover under load though, to see how it behaves.
Redis Sentinel failover does not guarantee consistency. WildDuck does not store critical information in Redis, so even if some data loss occurs, it should not
be noticeable.
### HAProxy
When using HAProxy you can enable PROXY protocol to get correct remote addresses in server logs. You can use the most basic round-robin based balancing as no
persistent sessions against specific hosts are needed. Use TCP load balancing with no extra settings both for plaintext and TLS connections.
If TLS is handled by HAProxy then use the following server config to indicate that WildDuck assumes to be a TLS server but TLS is handled upstream
```toml
[imap]
secure=true # this is a TLS server
secured=true # TLS is handled upstream
[pop3]
secure=true # this is a TLS server
secured=true # TLS is handled upstream
```
### Certificates
You can live-reload updated certificates by sending SIGHUP to the master process. This causes application configuration to be re-read from the disk. Reloading
only affects only some settings, for example all TLS certificates are loaded and updated. In this case existing processes continue as is, while new ones use the
updated certs.
Beware though that if configuration loading fails, then it ends with an exception. Make sure that TLS certificate files are readable for the WildDuck user.
## License

0
docs/.nojekyll Normal file
View file

View file

@ -1 +1 @@
api.wildduck.email
docs.wildduck.email

63
docs/README.md Normal file
View file

@ -0,0 +1,63 @@
# WildDuck Mail Server
WildDuck is a scalable no-SPOF IMAP/POP3 mail server. WildDuck uses a distributed database (sharded + replicated MongoDB) as a backend for storing all data,
including emails.
WildDuck tries to follow Gmail in product design. If there's a decision to be made then usually the answer is to do whatever Gmail has done.
## Contact
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/nodemailer/wildduck)
## Requirements
- _MongoDB_ to store all data
- _Redis_ for pubsub and counters
- _Node.js_ at least version 8.0.0
**Optional requirements**
- Redis Sentinel for automatic Redis failover
- Build tools to install optional dependencies that need compiling
WildDuck can be installed on any Node.js compatible platform.
## No-SPOF architecture
Every component of the WildDuck mail server can be replicated which eliminates potential single point of failures.
![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/wd.png)
## Storage
Attachment de-duplication and compression gives up to 56% of storage size reduction.
![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/storage.png)
## 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
## Future considerations
- 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.
- Maybe allow some kind of message manipulation through plugins
- WildDuck 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 WildDuck:
- IMAP4 non-synchronizing literals, LITERAL- ([RFC7888](https://tools.ietf.org/html/rfc7888)). Synchronized literals are needed for APPEND to check mailbox
quota, small values could go with the non-synchronizing version.
- LIST-STATUS ([RFC5819](https://tools.ietf.org/html/rfc5819))
- _What else?_ (definitely not NOTIFY nor QRESYNC)
## License
WildDuck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html) or later.

24
docs/_sidebar.md Normal file
View file

@ -0,0 +1,24 @@
- General
- [Info](README.md)
- [Features](general/features.md)
- [Installation](general/install.md)
- [Migration guide](general/migration-guide.md)
- [FAQ](general/faq.md)
- HTTP API
- [API Documentation :link:](//docs.wildduck.email/api)
- [Error codes](api-error-codes.md)
- Additional software
- [WildDuck MTA (Outbound SMTP)](additional-software/wildduck-mta.md)
- [Haraka plugin (Inbound SMTP)](additional-software/haraka-plugin.md)
- [Rspamd (Spam detection)](additional-software/rspamd.md)
- [WildDuck webmail](additional-software/webmail.md)
- [Import Maildir](additional-software/import-maildir.md)
- [Third party projects](additional-software/third-party-projects.md)
- In Depth
- [Operating WildDuck](in-depth/operating-wildduck.md)
- [E-Mail Protocol support](in-depth/protocol-support.md)
- [Security implementation](in-depth/security.md)
- [Administrating WildDuck via command line](in-depth/command-line.md)
- [Attachment deduplication](in-depth/attachment-deduplication.md)
- [Retention policies](in-depth/retention-policies.md)
- [Docker](in-depth/docker.md)

View file

@ -0,0 +1,3 @@
# Inbound SMTP
Use [Haraka](http://haraka.github.io/) with [haraka-plugins-wildduck](https://github.com/nodemailer/haraka-plugin-wildduck) to validate recipient addresses and quota usage against the WildDuck users database and to store/filter messages.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View file

@ -0,0 +1,3 @@
# Import maildir tool
You can use [import-maildir](https://github.com/nodemailer/import-maildir) tool to import maildir files straight to WildDuck database. This has less overhead than imap based imports, and you do not need to know the users password.

View file

@ -0,0 +1,3 @@
# Spam detection
Use [Rspamd plugin for Haraka](https://github.com/haraka/haraka-plugin-rspamd) in order to detect spam. WildDuck plugin detects Rspamd output and uses this information to send the message either to Inbox or Junk.

View file

@ -0,0 +1,23 @@
# Third party projects
This is an overview of any third party projects that integrate with or relate to WildDuck. This list is sorted by first commit date.
## Astzweig Docker Wildduck
https://github.com/astzweig/docker-wildduck
The famous nodemailer/wildduck email server as a docker container.
WildDuck, ZoneMTA, Haraka, rspamd in one image.
## DuckyPanel
https://github.com/louis-lau/DuckyPanel
DuckyPanel in combination with [DuckyAPI](https://github.com/louis-lau/DuckyAPI) is a domain admin control panel for WildDuck. It allows for multiple users, each owning specific domains. These users have full control over the domains and the accounts and forwarders within them.
![DuckyPanel Screenshot](img/duckypanel-screenshot.png)
## Raven Webmail
https://github.com/ramiroaisen/raven-webmail
A webmail for the wildduck mail server.
![Raven Webmail Screenshot](img/raven-screenshot.png)

View file

@ -0,0 +1,8 @@
# WildDuck webmail
This is the default web service for WildDuck. The web service uses the WildDuck API to manage user settings and preview messages.
## Live demo
There's a live demo up at https://wildduck.email you can register a free @wildduck.email email address and try it out as a real email account.
![](https://cldup.com/TZoTfxPugm.png)

View file

@ -0,0 +1,7 @@
# Outbound SMTP
Use [WildDuck MTA](https://github.com/nodemailer/wildduck-mta) (which under the hood is [ZoneMTA](https://github.com/zone-eu/zone-mta) with the
[ZoneMTA-WildDuck](https://github.com/nodemailer/zonemta-wildduck) plugin).
This gives you an outbound SMTP server that uses WildDuck accounts for authentication. The plugin authenticates user credentials and also rewrites headers if
needed (if the header From: address does not match user address or aliases then it is rewritten).

42
docs/api-error-codes.md Normal file
View file

@ -0,0 +1,42 @@
# API Error Codes
- `InvalidToken`
- `MissingPrivileges`: Not enough privileges
- `InputValidationError` (various descriptions about invalid input validation)
- `InternalDatabaseError`
- `InternalError`
- `UserNotFound`: This user does not exist
- `UserExistsError`: This username already exists
- `AddressExistsError`: Address already exists
- `AddressNotFound`: Invalid or unknown email address identifier
- `ChangeNotAllowed`: Can not change special address
- `WildcardNotPermitted`: Can not set wildcard address as default
- `AspNotFound`: Invalid or unknown ASP key
- `InvalidAuthScope`: Profile file requires either imap or pop3 and smtp scopes
- `AuthFailed`: Authentication failed
- `DkimNotFound`: This domain does not exist
- `AliasExists`: This domain alias already exists
- `AliasNotFound`: This alias does not exist
- `FilterNotFound`: This filter does not exist
- `NoSuchMailbox`: This mailbox does not exist
- `MessageNotFound`: Invalid message identifier
- `OVERQUOTA`: User is over quota
- `EmptyMessage`: Empty message provided
- `FileNotFound`: This file does not exist
- `InsecurePasswordError`: Provided password was found from breached passwords list
- `ERRREDIS`
- `ERRSENDINGLIMIT`: You reached a daily sending limit for your account
- `ERRCOMPOSE`: Could not queue message for delivery
- `SUBMITFAIL`
- `KeyGenereateError`: Failed to generate private or public key
- `InternalConfigError`: Invalid encryption settings
- `HashError`
- `UserUpdateFail`: Could not update user
- `TotpEnabled`: TOTP 2FA is already enabled for this user
- `QRError`: Failed to generate QR code
- `TotpDisabled`: TOTP 2FA is not initialized for this user
- `RateLimitedError`: Authentication was rate limited.
- `U2fEnabled`: U2F 2FA is already enabled for this user
- `U2fDisabled`: U2F 2FA is not initialized for this user
- `InvalidU2fRequest`: Failed to validate U2F response
- `NoUpdates`: Nothing was updated

View file

@ -1,6 +1,6 @@
# HTTP API
**DEPRECATED DOCS**, see https://api.wildduck.email
**DEPRECATED DOCS**, see https://docs.wildduck.email/api
WildDuck Mail Server is a scalable IMAP / POP3 server that natively exposes internal data through an HTTP API.

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View file

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 894 B

687
docs/api/index.html Normal file
View file

@ -0,0 +1,687 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Loading...</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="vendor/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="vendor/prettify.css" rel="stylesheet" media="screen">
<link href="css/style.css" rel="stylesheet" media="screen, print">
<link href="img/favicon.ico" rel="icon" type="image/x-icon">
<script src="vendor/polyfill.js"></script>
</head>
<body class="container-fluid">
<script id="template-sidenav" type="text/x-handlebars-template">
<nav id="scrollingNav">
<div class="sidenav-search">
<input class="form-control search" type="text" placeholder="{{__ "Filter..."}}">
<span class="search-reset">x</span>
</div>
<ul class="sidenav nav nav-list list">
{{#each nav}}
{{#if title}}
{{#if isHeader}}
{{#if isFixed}}
<li class="nav-fixed nav-header navbar-btn nav-list-item" data-group="{{group}}"><a href="#api-{{group}}" data-name="show-api-{{group}}" class="show-api api-{{group}}-init">{{underscoreToSpace title}}</a></li>
{{else}}
<li class="nav-header nav-list-item" data-group="{{group}}"><a href="#api-{{group}}" data-group="show-api-{{group}}" class="show-group api-{{group}}-init">{{underscoreToSpace title}}</a></li>
{{/if}}
{{else}}
<li class="{{#if hidden}}hide {{/if}}" data-group="{{group}}" data-name="{{name}}" data-version="{{version}}">
<a href="#api-{{group}}-{{name}}" title="{{url}}" data-group="show-api-{{group}}" data-name="show-api-{{group}}-{{name}}" class="nav-list-item show-api api-{{group}}-{{name}}-init">{{title}}<div class="nav-list-url-item hide">{{url}}</div></a>
</li>
{{/if}}
{{/if}}
{{/each}}
</ul>
</nav>
</script>
<script id="template-project" type="text/x-handlebars-template">
<div class="pull-left">
<h1>{{name}}</h1>
{{#if description}}<h2>{{{nl2br description}}}</h2>{{/if}}
</div>
{{#if template.withCompare}}
<div class="pull-right">
<div class="btn-group">
<button id="version" class="btn btn-lg btn-default dropdown-toggle" data-toggle="dropdown">
<strong>{{version}}</strong>&nbsp;<span class="caret"></span>
</button>
<ul id="versions" class="dropdown-menu open-left">
<li><a id="compareAllWithPredecessor" href="#">{{__ "Compare all with predecessor"}}</a></li>
<li class="divider"></li>
<li class="disabled"><a href="#">{{__ "show up to version:"}}</a></li>
{{#each versions}}
<li class="version"><a href="#">{{this}}</a></li>
{{/each}}
</ul>
</div>
</div>
{{/if}}
<div class="clearfix"></div>
</script>
<script id="template-header" type="text/x-handlebars-template">
{{#if content}}
<div id="api-_" class="show-api-article show-api-_-article">{{{content}}}</div>
{{/if}}
</script>
<script id="template-footer" type="text/x-handlebars-template">
{{#if content}}
<div id="api-_footer" class="show-api-article show-api-_-article">{{{content}}}</div>
{{/if}}
</script>
<script id="template-generator" type="text/x-handlebars-template">
{{#if template.withGenerator}}
{{#if generator}}
<div class="content">
{{__ "Generated with"}} <a href="{{{generator.url}}}">{{{generator.name}}}</a> {{{generator.version}}} - {{{generator.time}}}
</div>
{{/if}}
{{/if}}
</script>
<script id="template-sections" type="text/x-handlebars-template">
<section id="api-{{group}}" class="show-api-group show-api-{{group}}-group {{#if aloneDisplay}} hide{{/if}}">
<h1>{{underscoreToSpace title}}</h1>
{{#if description}}
<p>{{{nl2br description}}}</p>
{{/if}}
{{#each articles}}
<div id="api-{{group}}-{{name}}" class="show-api-article show-api-{{group}}-article show-api-{{group}}-{{name}}-article {{#if aloneDisplay}} hide{{/if}}">
{{{article}}}
</div>
{{/each}}
</section>
</script>
<script id="template-article" type="text/x-handlebars-template">
<article id="api-{{article.group}}-{{article.name}}-{{article.version}}" {{#if hidden}}class="hide"{{/if}} data-group="{{article.group}}" data-name="{{article.name}}" data-version="{{article.version}}">
<div class="pull-left">
<h1>{{underscoreToSpace article.groupTitle}}{{#if article.title}} - {{article.title}}{{/if}}</h1>
</div>
{{#if template.withCompare}}
<div class="pull-right">
<div class="btn-group">
<button class="version btn btn-default dropdown-toggle" data-toggle="dropdown">
<strong>{{article.version}}</strong>&nbsp;<span class="caret"></span>
</button>
<ul class="versions dropdown-menu open-left">
<li class="disabled"><a href="#">{{__ "compare changes to:"}}</a></li>
{{#each versions}}
<li class="version"><a href="#">{{this}}</a></li>
{{/each}}
</ul>
</div>
</div>
{{/if}}
<div class="clearfix"></div>
{{#if article.author}}<h4 class="muted">Authored by: {{article.author}}</h4>{{/if}}
{{#if article.deprecated}}
<p class="deprecated"><span>{{__ "DEPRECATED"}}</span>
{{{markdown article.deprecated.content}}}
</p>
{{/if}}
{{#if article.description}}
<p>{{{nl2br article.description}}}</p>
{{/if}}
<span class="type type__{{toLowerCase article.type}}">{{toLowerCase article.type}}</span>
<pre class="prettyprint language-html" data-type="{{toLowerCase article.type}}"><code>{{article.url}}</code></pre>
{{#if article.permission}}
<p>
{{__ "Permission:"}}
{{#each article.permission}}
{{name}}
{{#if title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{title}}" data-content="{{nl2br description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{/if}}
{{/each}}
</p>
{{/if}}
{{#if_gt article.examples.length compare=0}}
<ul class="nav nav-tabs nav-tabs-examples">
{{#each article.examples}}
<li{{#if_eq @index compare=0}} class="active"{{/if_eq}}>
<a href="#examples-{{../id}}-{{@index}}">{{title}}</a>
</li>
{{/each}}
</ul>
<div class="tab-content">
{{#each article.examples}}
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="examples-{{../id}}-{{@index}}">
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{content}}</code></pre>
</div>
{{/each}}
</div>
{{/if_gt}}
{{subTemplate "article-param-block" params=article.header _hasType=_hasTypeInHeaderFields section="header"}}
{{subTemplate "article-param-block" params=article.parameter _hasType=_hasTypeInParameterFields section="parameter"}}
{{subTemplate "article-param-block" params=article.success _hasType=_hasTypeInSuccessFields section="success"}}
{{subTemplate "article-param-block" params=article.error _col1="Name" _hasType=_hasTypeInErrorFields section="error"}}
{{subTemplate "article-sample-request" article=article id=id}}
</article>
</script>
<script id="template-article-param-block" type="text/x-handlebars-template">
{{#if params}}
{{#each params.fields}}
<h2>{{__ @key}}</h2>
<table>
<thead>
<tr>
<th style="width: 30%">{{#if ../_col1}}{{__ ../_col1}}{{else}}{{__ "Field"}}{{/if}}</th>
{{#if ../_hasType}}<th style="width: 10%">{{__ "Type"}}</th>{{/if}}
<th style="width: {{#if ../_hasType}}60%{{else}}70%{{/if}}">{{__ "Description"}}</th>
</tr>
</thead>
<tbody>
{{#each this}}
<tr>
<td class="code">{{{splitFill field "." "&nbsp;&nbsp;"}}}{{#if optional}} <span class="label label-optional">{{__ "optional"}}</span>{{/if}}</td>
{{#if ../../_hasType}}
<td>
{{{type}}}
</td>
{{/if}}
<td>
{{{nl2br description}}}
{{#if defaultValue}}<p class="default-value">{{__ "Default value:"}} <code>{{{defaultValue}}}</code></p>{{/if}}
{{#if size}}<p class="type-size">{{__ "Size range:"}} <code>{{{size}}}</code></p>{{/if}}
{{#if allowedValues}}<p class="type-size">{{__ "Allowed values:"}}
{{#each allowedValues}}
<code>{{{this}}}</code>{{#unless @last}}, {{/unless}}
{{/each}}
</p>
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/each}}
{{#if_gt params.examples.length compare=0}}
<ul class="nav nav-tabs nav-tabs-examples">
{{#each params.examples}}
<li{{#if_eq @index compare=0}} class="active"{{/if_eq}}>
<a href="#{{../section}}-examples-{{../id}}-{{@index}}">{{title}}</a>
</li>
{{/each}}
</ul>
<div class="tab-content">
{{#each params.examples}}
<div class="tab-pane{{#if_eq @index compare=0}} active{{/if_eq}}" id="{{../section}}-examples-{{../id}}-{{@index}}">
<pre class="prettyprint language-{{type}}" data-type="{{type}}"><code>{{reformat content type}}</code></pre>
</div>
{{/each}}
</div>
{{/if_gt}}
{{/if}}
</script>
<script id="template-article-sample-request" type="text/x-handlebars-template">
{{#if article.sampleRequest}}
<h2>{{__ "Send a Sample Request"}}</h2>
<form class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-md-3 control-label" for="{{../id}}-sample-request-url"></label>
<div class="input-group">
<input id="{{../id}}-sample-request-url" type="text" class="form-control sample-request-url" value="{{article.sampleRequest.0.url}}" />
<span class="input-group-addon">{{__ "url"}}</span>
</div>
</div>
{{#if article.header}}
{{#if article.header.fields}}
<h3>{{__ "Headers"}}</h3>
{{#each article.header.fields}}
<h4><input type="checkbox" data-sample-request-header-group-id="sample-request-header-{{@index}}" name="{{../id}}-sample-request-header" value="{{@index}}" class="sample-request-header sample-request-switch" checked />{{__ @key}}</h4>
<div class="{{../id}}-sample-request-header-fields">
{{#each this}}
<div class="form-group">
<label class="col-md-3 control-label" for="sample-request-header-field-{{field}}">{{field}}</label>
<div class="input-group">
<input type="text" placeholder="{{field}}" value="{{defaultValue}}" id="sample-request-header-field-{{field}}" class="form-control sample-request-header" data-sample-request-header-name="{{field}}" data-sample-request-header-group="sample-request-header-{{@../index}}">
<span class="input-group-addon">{{{type}}}</span>
</div>
</div>
{{/each}}
</div>
{{/each}}
{{/if}}
{{/if}}
{{#if article.parameter}}
{{#if article.parameter.fields}}
<h3>{{__ "Parameters"}}</h3>
{{#each article.parameter.fields}}
<h4><input type="checkbox" data-sample-request-param-group-id="sample-request-param-{{@index}}" name="{{../id}}-sample-request-param" value="{{@index}}" class="sample-request-param sample-request-switch" checked/>{{__ @key}}
<select name="{{../id}}-sample-header-content-type" class="{{../id}}-sample-request-param-select sample-header-content-type sample-header-content-type-switch">
<option value="undefined" selected>ajax-auto</option>
<option value="body-json" >body/json</option>
<option value="body-form-data" >body/form-data</option>
</select>
</h4>
<div class="{{../id}}-sample-request-param-body {{../id}}-sample-header-content-type-body hide">
<div class="form-group">
<div class="input-group">
<textarea id="sample-request-body-json" class="form-control sample-request-body" data-sample-request-body-group="sample-request-param-{{@./index}}" rows="6" style="OVERFLOW: visible" {{#if optional}}data-sample-request-param-optional="true"{{/if}}></textarea>
<div class="input-group-addon">json</div>
</div>
</div>
</div>
<div class="{{../id}}-sample-request-param-fields {{../id}}-sample-header-content-type-fields">
{{#each this}}
<div class="form-group">
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
<div class="input-group">
<input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}" {{#if optional}}data-sample-request-param-optional="true"{{/if}}>
<div class="input-group-addon">{{{type}}}</div>
</div>
</div>
{{/each}}
</div>
{{/each}}
{{/if}}
{{/if}}
<div class="form-group">
<div class="controls pull-right">
<button class="btn btn-primary sample-request-send" data-sample-request-type="{{article.type}}">{{__ "Send"}}</button>
</div>
</div>
<div class="form-group sample-request-response" style="display: none;">
<h3>
{{__ "Response"}}
<button class="btn btn-default btn-xs pull-right sample-request-clear">X</button>
</h3>
<pre class="prettyprint language-json" data-type="json"><code class="sample-request-response-json"></code></pre>
</div>
</fieldset>
</form>
{{/if}}
</script>
<script id="template-compare-article" type="text/x-handlebars-template">
<article id="api-{{article.group}}-{{article.name}}-{{article.version}}" {{#if hidden}}class="hide"{{/if}} data-group="{{article.group}}" data-name="{{article.name}}" data-version="{{article.version}}" data-compare-version="{{compare.version}}">
<div class="pull-left">
<h1>{{underscoreToSpace article.group}} - {{{showDiff article.title compare.title}}}</h1>
</div>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-success" disabled>
<strong>{{article.version}}</strong> {{__ "compared to"}}
</button>
<button class="version btn btn-danger dropdown-toggle" data-toggle="dropdown">
<strong>{{compare.version}}</strong>&nbsp;<span class="caret"></span>
</button>
<ul class="versions dropdown-menu open-left">
<li class="disabled"><a href="#">{{__ "compare changes to:"}}</a></li>
<li class="divider"></li>
{{#each versions}}
<li class="version"><a href="#">{{this}}</a></li>
{{/each}}
</ul>
</div>
</div>
<div class="clearfix"></div>
{{#if article.description}}
<p>{{{showDiff article.description compare.description "nl2br"}}}</p>
{{else}}
{{#if compare.description}}
<p>{{{showDiff "" compare.description "nl2br"}}}</p>
{{/if}}
{{/if}}
<pre class="prettyprint language-html" data-type="{{toLowerCase article.type}}"><code>{{{showDiff article.url compare.url}}}</code></pre>
{{subTemplate "article-compare-permission" article=article compare=compare}}
<ul class="nav nav-tabs nav-tabs-examples">
{{#each_compare_title article.examples compare.examples}}
{{#if typeSame}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#compare-examples-{{../../article.id}}-{{index}}">{{{showDiff source.title compare.title}}}</a>
</li>
{{/if}}
{{#if typeIns}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#compare-examples-{{../../article.id}}-{{index}}"><ins>{{{source.title}}}</ins></a>
</li>
{{/if}}
{{#if typeDel}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#compare-examples-{{../../article.id}}-{{index}}"><del>{{{compare.title}}}</del></a>
</li>
{{/if}}
{{/each_compare_title}}
</ul>
<div class="tab-content">
{{#each_compare_title article.examples compare.examples}}
{{#if typeSame}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{source.type}}"><code>{{{showDiff source.content compare.content}}}</code></pre>
</div>
{{/if}}
{{#if typeIns}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{source.type}}"><code>{{{source.content}}}</code></pre>
</div>
{{/if}}
{{#if typeDel}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{compare.type}}"><code>{{{compare.content}}}</code></pre>
</div>
{{/if}}
{{/each_compare_title}}
</div>
{{subTemplate "article-compare-param-block" source=article.parameter compare=compare.parameter _hasType=_hasTypeInParameterFields section="parameter"}}
{{subTemplate "article-compare-param-block" source=article.success compare=compare.success _hasType=_hasTypeInSuccessFields section="success"}}
{{subTemplate "article-compare-param-block" source=article.error compare=compare.error _col1="Name" _hasType=_hasTypeInErrorFields section="error"}}
{{subTemplate "article-sample-request" article=article id=id}}
</article>
</script>
<script id="template-article-compare-permission" type="text/x-handlebars-template">
<p>
{{__ "Permission:"}}
{{#each_compare_list_field article.permission compare.permission field="name"}}
{{#if source}}
{{#if typeSame}}
{{source.name}}
{{#if source.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{source.title}}" data-content="{{nl2br source.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{#if typeIns}}
<ins>{{source.name}}</ins>
{{#if source.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{source.title}}" data-content="{{nl2br source.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{#if typeDel}}
<del>{{source.name}}</del>
{{#if source.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{source.title}}" data-content="{{nl2br source.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{else}}
{{#if typeSame}}
{{compare.name}}
{{#if compare.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{compare.title}}" data-content="{{nl2br compare.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{#if typeIns}}
<ins>{{compare.name}}</ins>
{{#if compare.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{compare.title}}" data-content="{{nl2br compare.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{#if typeDel}}
<del>{{compare.name}}</del>
{{#if compare.title}}
<button type="button" class="btn btn-info btn-xs" data-title="{{compare.title}}" data-content="{{nl2br compare.description}}" data-html="true" data-toggle="popover" data-placement="right" data-trigger="hover">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</button>
{{#unless _last}}, {{/unless}}
{{/if}}
{{/if}}
{{/if}}
{{/each_compare_list_field}}
</p>
</script>
<script id="template-article-compare-param-block" type="text/x-handlebars-template">
{{#if source}}
{{#each_compare_keys source.fields compare.fields}}
{{#if typeSame}}
<h2>{{__ source.key}}</h2>
<table>
<thead>
<tr>
<th style="width: 30%">{{#if ../_col1}}{{__ ../_col1}}{{else}}{{__ "Field"}}{{/if}}</th>
{{#if ../_hasType}}<th style="width: 10%">{{__ "Type"}}</th>{{/if}}
<th style="width: {{#if ../_hasType}}60%{{else}}70%{{/if}}">{{__ "Description"}}</th>
</tr>
</thead>
{{subTemplate "article-compare-param-block-body" source=source.value compare=compare.value _hasType=../_hasType}}
</table>
{{/if}}
{{#if typeIns}}
<h2><ins>{{__ source.key}}</ins></h2>
<table class="ins">
<thead>
<tr>
<th style="width: 30%">{{#if ../_col1}}{{__ ../_col1}}{{else}}{{__ "Field"}}{{/if}}</th>
{{#if ../_hasType}}<th style="width: 10%">{{__ "Type"}}</th>{{/if}}
<th style="width: {{#if ../_hasType}}60%{{else}}70%{{/if}}">{{__ "Description"}}</th>
</tr>
</thead>
{{subTemplate "article-compare-param-block-body" source=source.value compare=source.value _hasType=../_hasType}}
</table>
{{/if}}
{{#if typeDel}}
<h2><del>{{__ compare.key}}</del></h2>
<table class="del">
<thead>
<tr>
<th style="width: 30%">{{#if ../_col1}}{{__ ../_col1}}{{else}}{{__ "Field"}}{{/if}}</th>
{{#if ../_hasType}}<th style="width: 10%">{{__ "Type"}}</th>{{/if}}
<th style="width: {{#if ../_hasType}}60%{{else}}70%{{/if}}">{{__ "Description"}}</th>
</tr>
</thead>
{{subTemplate "article-compare-param-block-body" source=compare.value compare=compare.value _hasType=../_hasType}}
</table>
{{/if}}
{{/each_compare_keys}}
{{#if source.examples}}
<ul class="nav nav-tabs nav-tabs-examples">
{{#each_compare_title source.examples compare.examples}}
{{#if typeSame}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#{{../../section}}-compare-examples-{{../../article.id}}-{{index}}">{{{showDiff source.title compare.title}}}</a>
</li>
{{/if}}
{{#if typeIns}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#{{../../section}}-compare-examples-{{../../article.id}}-{{index}}"><ins>{{{source.title}}}</ins></a>
</li>
{{/if}}
{{#if typeDel}}
<li{{#if_eq index compare=0}} class="active"{{/if_eq}}>
<a href="#{{../../section}}-compare-examples-{{../../article.id}}-{{index}}"><del>{{{compare.title}}}</del></a>
</li>
{{/if}}
{{/each_compare_title}}
</ul>
<div class="tab-content">
{{#each_compare_title source.examples compare.examples}}
{{#if typeSame}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="{{../../section}}-compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{source.type}}"><code>{{{showDiff source.content compare.content}}}</code></pre>
</div>
{{/if}}
{{#if typeIns}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="{{../../section}}-compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{source.type}}"><code>{{{source.content}}}</code></pre>
</div>
{{/if}}
{{#if typeDel}}
<div class="tab-pane{{#if_eq index compare=0}} active{{/if_eq}}" id="{{../../section}}-compare-examples-{{../../article.id}}-{{index}}">
<pre class="prettyprint language-{{source.type}}" data-type="{{compare.type}}"><code>{{{compare.content}}}</code></pre>
</div>
{{/if}}
{{/each_compare_title}}
</div>
{{/if}}
{{/if}}
</script>
<script id="template-article-compare-param-block-body" type="text/x-handlebars-template">
<tbody>
{{#each_compare_field source compare}}
{{#if typeSame}}
<tr>
<td class="code">
{{{splitFill source.field "." "&nbsp;&nbsp;"}}}
{{#if source.optional}}
{{#if compare.optional}} <span class="label label-optional">{{__ "optional"}}</span>
{{else}} <span class="label label-optional label-ins">{{__ "optional"}}</span>
{{/if}}
{{else}}
{{#if compare.optional}} <span class="label label-optional label-del">{{__ "optional"}}</span>{{/if}}
{{/if}}
</td>
{{#if source.type}}
{{#if compare.type}}
<td>{{{showDiff source.type compare.type}}}</td>
{{else}}
<td>{{{source.type}}}</td>
{{/if}}
{{else}}
{{#if compare.type}}
<td>{{{compare.type}}}</td>
{{else}}
{{#if ../../../../_hasType}}<td></td>{{/if}}
{{/if}}
{{/if}}
<td>
{{{showDiff source.description compare.description "nl2br"}}}
{{#if source.defaultValue}}<p class="default-value">{{__ "Default value:"}} <code>{{{showDiff source.defaultValue compare.defaultValue}}}</code><p>{{/if}}
</td>
</tr>
{{/if}}
{{#if typeIns}}
<tr class="ins">
<td class="code">
{{{splitFill source.field "." "&nbsp;&nbsp;"}}}
{{#if source.optional}} <span class="label label-optional label-ins">{{__ "optional"}}</span>{{/if}}
</td>
{{#if source.type}}
<td>{{{source.type}}}</td>
{{else}}
{{{typRowTd}}}
{{/if}}
<td>
{{{nl2br source.description}}}
{{#if source.defaultValue}}<p class="default-value">{{__ "Default value:"}} <code>{{{source.defaultValue}}}</code><p>{{/if}}
</td>
</tr>
{{/if}}
{{#if typeDel}}
<tr class="del">
<td class="code">
{{{splitFill compare.field "." "&nbsp;&nbsp;"}}}
{{#if compare.optional}} <span class="label label-optional label-del">{{__ "optional"}}</span>{{/if}}
</td>
{{#if compare.type}}
<td>{{{compare.type}}}</td>
{{else}}
{{{typRowTd}}}
{{/if}}
<td>
{{{nl2br compare.description}}}
{{#if compare.defaultValue}}<p class="default-value">{{__ "Default value:"}} <code>{{{compare.defaultValue}}}</code><p>{{/if}}
</td>
</tr>
{{/if}}
{{/each_compare_field}}
</tbody>
</script>
<div class="container-fluid">
<div class="row">
<div id="sidenav" class="span2"></div>
<div id="content">
<div id="project"></div>
<div id="header"></div>
<div id="sections"></div>
<div id="footer"></div>
<div id="generator"></div>
</div>
</div>
</div>
<div id="loader">
<div class="spinner">
<div class="spinner-container container1">
<div class="circle1"></div><div class="circle2"></div><div class="circle3"></div><div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div><div class="circle2"></div><div class="circle3"></div><div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div><div class="circle2"></div><div class="circle3"></div><div class="circle4"></div>
</div>
<p>Loading...</p>
</div>
</div>
<script data-main="main.js" src="vendor/require.min.js"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more