Run your own Exchange4all deployment¶
Run the only mail and calendaring server that is both fully MAPI compatible and not limited to running only on Windows Server operating systems, and that ships as a single container you deploy yourself.
This page describes docker compose
That is what our setup script automates and what we test against. The image itself is an ordinary
OCI container and runs on other runtimes as well, as long as you reproduce the same configuration:
host networking, the bind mounts, the environment variables and the raised file descriptor limit.
The generated docker-compose.yml is the reference for all
of it. On that path you configure and update the deployment yourself, because setup.sh requires
docker and docker compose.
Runtime Requirements¶
Your host needs to meet the following minimums:
| Requirement | Details |
|---|---|
| CPU and memory | 8 CPU cores and 64GB of RAM. The slim variant needs 4 vCPUs and 16GB. |
| Container runtime | Anything OCI compliant. With docker compose, use 1.29.2 or newer — older versions are not part of our testing pipeline. |
| Tools | docker, curl and jq. The setup script uses the latter two to look up the current container version. |
| Addressing | A dedicated IP, or at least exclusive access to ports 25, 80 and 443. The optional adsredir service needs a second dedicated IP. |
| Firewall | Run the container in host mode for IPv6 access, and restrict access to the publicly required ports yourself. |
Do not put this behind a load balancer or reverse proxy
The container terminates TLS itself, and deploying it behind an SSL-terminating gateway is not supported. If you must forward traffic to it, use a TLS proxy — an HTTP proxy is neither recommended nor supported.
Every externally reachable service also needs to see the real client IP address, for both IPv4 and IPv6, because rate limiting and other checks depend on it. Preserve those addresses in whatever deployment you build. Running the container in host network is the simplest way to get this right.
Quick Start¶
This chapter takes you from an empty host to a running deployment with your first domain and users.
Before you begin¶
Have the following ready. The setup script checks all of it and tells you what is missing, but resolving these first turns the installation into a single uninterrupted run:
- DNS. An
Aand, where available, anAAAArecord for the FQDN of the deployment, pointing at this host. If mail uses a different domain, a record forautodiscover.MAILDOMAINas well. DNS changes take time to propagate, so make them before you start: certificates and licence activation both fail while the names do not resolve. - Ports. 25, 80, 443, 587, 993 and 995 free on the host, and reachable from the internet. Port 25 outbound is also needed to deliver mail.
- A firewall, or a cloud security group, that exposes only those ports. The container uses host networking, see ports that must not be reachable.
- Registry credentials, see the next section, plus
docker,curlandjqinstalled. - Your licence, as the line of text you received. Without it only the webmail client works.
Installation and administration¶
Download the setup script into an empty directory and run it. It is the only file you need — it writes the docker-compose.yml and the .env file for you.
mkdir -p /opt/exchange4all && cd /opt/exchange4all
curl -fsSLO https://manual.myexchange.rocks/on-premise/setup.sh
chmod +x setup.sh
./setup.sh
To verify what you downloaded, compare it against SHA256SUMS.
Once the script has run, you have the following files:
| File or folder | What it is for |
|---|---|
setup.sh |
Configures and starts the deployment. Re-run it whenever you want to change a setting. |
docker-compose.yml |
Configuration of the container. Generated by setup.sh, do not edit it. |
docker-compose.override.yml |
Optional, and created by you. Every change of your own belongs here, see changing the container with an override file. |
exchange4all-config.yml |
Configuration of the application, as a symlink into the data location. Created by setup.sh. |
.env |
Configuration of the container. Created with mode 0600 because it holds secrets. |
/etc/systemd/system/exchange4all.service |
Optional. Lets you run systemctl start\|stop\|restart exchange4all. Offered during setup. |
~/.local/bin/e4a-cmd |
Runs setup.sh of this deployment from any directory, see running the setup script from anywhere. |
/srv/exchange4all |
Where Exchange4all stores its data on the host. Its subfolders are mounted into the container separately, see below. |
The data location holds one folder per mount, Backup describes what each one needs:
| On the host | Inside the container | What it holds |
|---|---|---|
/srv/exchange4all/e4a |
/storage |
Mail stores, configuration, licences, certificates and logs. |
/srv/exchange4all/db |
/var/lib/mysql |
The MariaDB database. |
/srv/exchange4all/php |
/var/lib/php/sessions |
Webmail sessions. |
/storage always means the path inside the container
Wherever this page mentions /storage, it refers to the storage location within the container. On the host that is the e4a folder of the data location, /srv/exchange4all/e4a by default. The data location can be changed during setup.
Back up the data location and .env together
The .env file holds the configuration your deployment was built with. A data directory without it is significantly harder to bring back up. Include both in your backups from day one.
Changing the container with an override file¶
setup.sh owns docker-compose.yml and rewrites it whenever you re-run the script, for example on --update. Anything you edit in that file is lost. Put your own changes in docker-compose.override.yml instead, which the script never touches.
Compose loads that file automatically and merges it into the generated one, so you only write the parts you are changing:
services:
exchange4all:
environment:
- TESTING_LOGGING=true
Apply your changes by recreating the container:
docker compose up -d
A few points that save time later:
- Use one override file. Compose only picks up
docker-compose.override.ymlon its own. Several sections of this page show override snippets — mount your own SSL certificates, enable core dumps, turn on debug logging — and if you need more than one, merge them into that single file. - You only add what you change. Volumes are merged by their path inside the container, and environment variables by name, so the generated mounts and variables stay in place. Use the same path or name when you deliberately want to replace one of them.
- Check the result before restarting anything:
docker compose configprints the merged configuration compose will actually use.
Prefer .env where a variable already exists
The generated compose file passes a number of environment variables straight through from .env, see Optional Environment Variables. Setting one of those needs no override file at all.
Completing the setup by running the setup script¶
The container registry requires a subscription
The images are published to registry.kopano.cloud. Access requires a subscription. You log in
with your email address as the user name and the access token you received as the password. If
you lost your credentials, please contact support.
When no credentials for this registry are present, the setup script asks for them and runs
docker login for you. The token is handed to docker only and never written to .env. For a
non-interactive run, pass --registry-user and --registry-token, or set E4A_REGISTRY_USER and
E4A_REGISTRY_TOKEN. With --registry-token - the token is read from standard input, which
keeps it out of your shell history:
./setup.sh --update --registry-user you@example.com --registry-token - < token.txt
Logging in by hand with docker login registry.kopano.cloud works just as well. The available
images and their tags are listed on the container images page.
The script asks you the questions below. Press Enter to accept the default shown in square brackets.
./setup.sh
What the setup script checks first¶
Before writing anything, the script inspects your host and prints one line per check. Failures stop the run, warnings do not.
These problems abort the setup, because continuing would give you a broken deployment:
| Check | Meaning |
|---|---|
docker |
Docker is not installed, the daemon is not running, or this user may not talk to it. |
compose |
Neither docker compose nor docker-compose 1.29.2 or newer is available. |
curl, jq |
Missing. jq is only needed when the container version still has to be looked up, so --image-version avoids it. |
fqdn |
The given name is not a fully qualified domain name. |
storage |
The data directory is not an absolute path, or its parent is not writable. |
port 25 … |
One of the ports 25, 80, 443, 587, 993 and 995 is already used by another service. Skipped when this deployment is already running. |
kernel |
The host does not run Linux. |
These are warnings. Each one comes with the command that resolves it, and the setup continues:
| Check | Meaning |
|---|---|
cpu, memory |
Fewer cores or less memory than recommended for the selected variant. |
storage |
Less than 50 GB free at the data location. |
docker.service |
Not enabled, so the container will not return after a reboot. |
clock |
The system clock is not synchronised. Certificates, licence activation and logins depend on the correct time. |
pending |
A docker-compose.yml.new or exchange4all.service.new waits next to a file the script did not replace because it was changed locally. |
dns |
A name in E4AFQDN or DOMAINS does not resolve, or resolves to an address that is not this host, including a stale AAAA record. Behind a load balancer or port forwarding this is expected, verify it from the outside. |
mx |
A mail domain has no MX record, or its MX points somewhere else. That is fine behind a mail gateway. |
spf |
A mail domain has no SPF record, more than one, or one that does not allow this host. Mechanisms such as include: are named but not followed. |
autodiscover |
autodiscover.DOMAIN does not resolve, or is missing from DOMAINS and therefore from the certificate. |
rdns |
The reverse DNS of the public IPv4 or IPv6 address is missing or not the FQDN. Set it at your hosting provider. |
smtp-out |
This host cannot reach other mail servers on port 25, which many providers block on new accounts. Ask them to lift the block, or use a mail relay. |
registry |
No credentials for the container registry, see above. |
firewall |
No active firewall, or one that allows an internal port. See required ports. |
variant |
A preview build was selected, which is not supported for production use. |
The mail domains checked are E4AMAILDOMAIN and every domain an autodiscover. name in DOMAINS is requested for. The DNS checks need dig or host, from the dnsutils or bind-utils package. The public addresses of the host are looked up at ip.me.
To run only these checks against an existing deployment, use ./setup.sh --check.
Running the setup non-interactively¶
Answer every question on the command line, or pass the values through the environment under their .env names. With -y the script never asks anything:
./setup.sh -y \
--fqdn mail.example.com \
--mail-domain example.com \
--le-email admin@example.com \
--acme staging \
--registry-user admin@example.com \
--registry-token "3f9a1c07e5b2..." \
--license "eyJhbGciOiJFZERTQSIsImtpZCI6..."
The same run driven entirely by the environment, which suits cloud-init and configuration management:
#!/bin/sh
export E4AFQDN="mail.example.com"
export E4AMAILDOMAIN="example.com"
export LEEMAIL="admin@example.com"
curl -fsSLO https://manual.myexchange.rocks/on-premise/setup.sh
sh setup.sh -y
./setup.sh --help lists every option. The most useful ones are --variant to pick the image, --update to move to a newer version, --check to run only the checks, and --dry-run to see what a run would do without writing anything.
The administrator password is only printed on a terminal
When the output is not a terminal, the summary prints the command to retrieve the password instead of the password itself, so it does not end up in cloud-init or CI logs.
Domains¶
The container creates SSL certificates for the name it is reached at, the FQDN. If you use a different domain for email, the MAILDOMAIN, it also needs a certificate for autodiscover.MAILDOMAIN. Make sure both names resolve to this host before you continue.
FQDN of this deployment [exchange4all.local]: mail.example.com
Domain to use for email [mail.example.com]: example.com
Domains to request certificates for, separated by spaces [autodiscover.example.com mail.example.com]:
The third question is answered for you: when the mail domain differs from the FQDN, autodiscover.MAILDOMAIN is added automatically. Add further names only if you want the deployment to be reachable under them as well. Every name in this list must resolve to this host — if one of them does not, certificate retrieval and licence activation fail for all of them.
Outlook and mobile ActiveSync clients use "autodiscover" to configure an account from nothing but an email address and a password, which is what the autodiscover subdomain is for. You can also point a reverse proxy from https://your-mail-domain.com/Autodiscover/Autodiscover.xml at your deployment, see Reverse Proxy configurations. At a minimum, confirm that this address does not redirect to a conflicting service.
Hosting Static Landing Pages for Unmatched URLs
A static file server answers any URL that does not match a registered client path, so you can serve simple landing pages or redirects without running a second backend.
Put your files in /storage/www inside the container. To send users who land on a subdomain such as autodiscover.exchange4all.local to your main website, create /storage/www/index.html:
<html>
<head>
<script>
window.location.href = "https://exchange4all.local/webapp/";
</script>
</head>
</html>
Every unmatched request now redirects to that address.
Automatic SSL with Let's Encrypt¶
By default the container issues a self-signed certificate from its own CA, whose root certificate you can download from http://your-server.com/.well-known/root-ca.crt. Give the setup script an email address instead and it retrieves a certificate from Let's Encrypt:
Email address for Let's Encrypt, empty for a self-signed certificate []: your@email.com
ACME endpoint, use production once retrieval works [staging]:
By filling in the email address you agree to the Let's Encrypt Terms of Service. The second question only appears once you have given an address.
Verify that certificates can be retrieved from the staging environment first. Once that works, switch over with ./setup.sh --acme production, or set ACMESERVER in the .env file by hand.
A note on using the Let's Encrypt Staging CA
Use the staging CA to test certificate retrieval, not to test Exchange4all itself. Work through it in this order:
- Leave the email field empty and use a self-signed certificate from the container's own CA while you evaluate.
- When you are ready to expose the server publicly, set
LEEMAILto your address, or re-runsetup.sh. - Confirm the container retrieves a certificate.
- Switch to the production CA with
./setup.sh --acme production.
Renewing the certificate right away¶
The container renews its Let's Encrypt certificate on its own. To get a new one immediately, for example after adding names to DOMAINS or to test the rotation, run:
./setup.sh --renew-certificate
It first checks that every name in DOMAINS resolves to this host, because a failed challenge counts against a rate limit of its own. With the production CA it asks before requesting, since Let's Encrypt issues at most five certificates for the same names per week. The new certificate is applied right away, and the script shows how long the certificate now served on port 443 is valid.
Debugging certificate rotation
Inside the container, a renewal is forced with lego-renew add up to version 8.7, and with LEGO_RENEW_FORCE=true lego-renew from version 8.8 onwards. The hook that applies a renewed certificate can also be run by hand, to debug the rotation itself. lego names the certificate after the first name in DOMAINS, and keeps staging certificates in /storage/.lego-tmp instead of /storage/.lego:
CERT_NAME=$(jq -r .DOMAINS /etc/container_environment.json | awk '{ print $1 }')
LEGO_CERT_PATH=/storage/.lego/certificates/$CERT_NAME.crt \
LEGO_CERT_KEY_PATH=/storage/.lego/certificates/$CERT_NAME.key \
/usr/local/bin/lego-hook
Reusing existing SSL certificates¶
To use certificates you already have, leave the Let's Encrypt address empty and mount them into the container yourself. Add the following to your override file, docker-compose.override.yml, next to the generated docker-compose.yml:
services:
exchange4all:
volumes:
- ./private.key:/opt/exchange4all/tls/fullchain.pem.key:ro
- ./certificate.crt:/opt/exchange4all/tls/fullchain.pem:ro
If the certificate comes from an internal CA, mount that root certificate as well by adding one more line to the volumes block above. Recreate the container with docker compose up -d once the file is complete:
- ./rootCA.pem:/opt/exchange4all/tls/root-ca.crt:ro
Data storage¶
Where to store data [/srv/exchange4all]
Most environments can keep the default. The setup script warns you when less than 50GB are free at the location you choose.
Changing this later does not move your data
This value only updates the configuration. If you change it on an existing deployment, move the contents of the old directory yourself before starting the container again.
Container variant¶
Finally the script asks which image to run:
Which container variant should this deployment run?
1) full - all components (8 cores, 64 GB RAM)
2) slim - reduced footprint (4 cores, 16 GB RAM)
3) next - preview build, not for production use
Selection [1]:
Your choice is stored as E4AVARIANT in the .env file and decides which image ./setup.sh --update picks up later, so a slim deployment stays slim. It also adjusts what the checks expect of your CPU and memory. Versions are always pinned by digest, and beta releases are never selected automatically.
Starting at boot¶
If the host uses systemd, the script offers to install a unit:
Install a systemd unit for systemctl start/stop/restart exchange4all? [Y/n]:
Treat the unit as a convenience, not as a supervisor. The container carries restart: unless-stopped, and that is what brings it back after a crash or a reboot, whether or not you install the unit. An existing unit file is never overwritten.
Enable the Docker service as well
The container can only return after a reboot if Docker itself starts at boot. The setup script warns you when it does not; systemctl enable docker fixes it.
First start¶
With the configuration in place, the script pulls the image and starts the container. The first startup runs a set of initialisation tasks and takes a few minutes. The script waits until the container reports itself healthy, then prints a summary:
==> Exchange4all is running
Configuration /opt/exchange4all/.env (mode 0600, keep this in your backups)
Compose file /opt/exchange4all/docker-compose.yml (generated, edit the override instead)
Data /srv/exchange4all (keep this in your backups)
Image registry.kopano.cloud/e4a-container:8.7.4@sha256:f68198...
Health healthy after 252s
Licence Testing <feedback@example.com>
Service systemctl start|stop|restart exchange4all
Manage console https://mail.example.com/manage/
User admin@example.com
Password docker compose exec exchange4all cat /storage/admin.secret
If the container is not healthy within 300 seconds, the script prints the last 50 log lines and exits with an error. Your container keeps starting regardless — on a slow connection the first image pull alone can take longer than that. Follow it with docker compose logs -f, or allow more time on the next run with --timeout 900.
Check on progress at any point with docker compose ps and docker compose logs -f. Once the container has finished starting, continue with your first domain and users.
Installing a licence file¶
Exchange4all needs an installed and activated licence. Your licence is a single long line of text, and the setup script asks you to paste it:
Paste your Exchange4all licence, empty to skip:
The script shows you who the licence belongs to and asks for confirmation before installing it. It lands in /storage/license/exchange4all.license, and because that happens before the first start, the container activates it on its own.
If your licence arrives later, or you renew it, add it at any time:
./setup.sh --license "eyJhbGciOiJFZERTQSIsImtpZCI6..."
On a deployment that was set up before, --license installs and activates only the licence. It asks no setup questions and does not restart the container. A licence previously installed by the script is kept as exchange4all.license.bak-<date>, and other .license files in the directory stay in place. Every run of the script lists the installed licences in its summary.
Activation needs your domains to resolve
Activation performs an HTTP-01 challenge for every name in DOMAINS, so all of them must point at this host. If it fails, fix the DNS entries and run docker compose exec exchange4all /usr/local/bin/lx-renew register.
To install a licence by hand, copy the file into /storage/license/ and run /usr/local/bin/lx-renew register inside the container. The extension matters — only files ending in .license are read.
Without an activated licence, only webmail works
Your users can log in to the Exchange4all webmail client, but connections via Outlook, ActiveSync and IMAP/POP3 are denied. Users with the System Administrator role receive nightly reminders about upcoming and passed licence expiry.
Configuring defaults for MX, SPF and Autodiscover¶
The Manage console tells your administrators which MX and SPF records to create, and how clients find autodiscover. Those instructions are generated from the container's FQDN unless you configure the values in /storage/config.yaml.
To publish your own values, add the following to /storage/config.yaml and adjust it to your environment:
e4a:
# skipping unrelated content
domain_service:
dns:
- type: mx
value: "mail.protection.exchange4all.local"
- type: spf
value: "v=spf1 include:spf.protection.exchange4all.local -all"
- type: autodiscover
value: ""
Match the autodiscover value to the domain in your _autodiscover._tcp SRV record, which is what clients query to locate the service.
Autodiscover recommendations for customer provided domains
If you host domains for your customers, use the "adsredir" service instead of listing every autodiscover name yourself. It requests SSL certificates on demand from Let's Encrypt, or any other service implementing the ACME protocol. See /etc/service/e4a-service-adsredird/run in the container for runtime instructions.
Creating Users, Domains and Organisations¶
With the container running, create your domains and users in the Manage console at https://your-server.com/manage/, or through the manage API behind it. The setup script prints the address and your credentials at the end of its run.
Note
When the container is started for the first time, it automatically creates:
- an organisation named "Default",
- a domain named after the mail domain (
E4AMAILDOMAIN) you gave the setup script, belonging to the above organisation - a user
admin@MAILDOMAINwith "system administrator" privileges belonging to the above domain.
The generated password of that user is stored in /storage/admin.secret and can be read with:
docker compose exec exchange4all cat /storage/admin.secret
As a system administrator you can:
- Create organisations, often called tenants. They are optional and group domains together. Users of domains in the same organisation see each other in the global address list (GAL).
- Create email domains, and within each of them users, groups and aliases.
For step-by-step instructions on the Manage console, see Information for Administrators.
Creating them through the API¶
The Manage console uses the Manage API, which also answers on a unix socket inside the container, without a login. A script can set up organisations, domains, users, groups and aliases through it, for example to prepare a testing system. This one creates an organisation with two domains and some users in them. Each object is only created when it does not exist yet, so the script can run more than once:
#!/bin/bash
# Creates an organisation with two domains, users, a group and aliases.
# Run it inside the container: e4a-cmd -q --exec bash -s < provision.sh
set -eu
api() {
curl -s --unix-socket /var/run/exchange4all-manage-api/rest0.sock "$@"
}
# create PATH COLLECTION JSON: creates an object unless one exists at PATH already.
create() {
if ! curl -sf --unix-socket /var/run/exchange4all-manage-api/rest0.sock "http://manage-api/api/v1/$1" >/dev/null; then
api "http://manage-api/api/v1/$2" -H 'content-type: application/json' --data-raw "$3"
echo
fi
}
create organizations/Demo organizations '{"id":"Demo"}'
create domains/example.com domains '{"id":"example.com","e4a":{"endDay":null}}'
create domains/example.net domains '{"id":"example.net","e4a":{"endDay":null}}'
# Replaces the domains of the organisation, list every domain it should keep.
api http://manage-api/api/v1/organizations/Demo -X PATCH -H 'content-type: application/json' \
--data-raw '{"verifiedDomains":[{"name":"example.com"},{"name":"example.net"}]}'
# privilegeBits 1031 makes Markus an organisation administrator, maxSize is in bytes.
create users/markus@example.com users '{"accountEnabled":true,"displayName":"Markus Bürger","mailNickname":"markus","userPrincipalName":"markus@example.com","passwordProfile":{"password":"change-me"},"mailboxSettings":{"language":{"locale":"en-GB"}},"e4a":{"status":0,"privilegeBits":1031,"maxSize":10737418240}}'
create users/sophie@example.com users '{"accountEnabled":true,"displayName":"Sophie Schulz","mailNickname":"sophie","userPrincipalName":"sophie@example.com","passwordProfile":{"password":"change-me"}}'
create users/leonie@example.net users '{"accountEnabled":true,"displayName":"Leonie Propst","mailNickname":"leonie","userPrincipalName":"leonie@example.net","passwordProfile":{"password":"change-me"}}'
create groups/sales@example.com groups '{"displayName":"Sales","groupTypes":["Unified"],"mail":"sales@example.com","mailNickname":"sales","allowExternalSenders":true,"mailEnabled":true,"securityEnabled":false,"visibility":"HiddenMembership","members":[{"mail":"markus@example.com"},{"mail":"leonie@example.net"}]}'
# An additional address for Markus, and a domain whose mail goes to example.com.
create e4a/aliases/ceo@example.com e4a/aliases '{"mail":"ceo@example.com","e4a":{"userPrincipalName":"markus@example.com"}}'
create e4a/aliases/example-alias.com e4a/aliases '{"id":"example-alias.com","e4a":{"canonicalName":"example.com"}}'
# Resource accounts for rooms and equipment.
create users/meeting@example.com users '{"accountEnabled":true,"displayName":"Meeting Room","mailNickname":"meeting","userPrincipalName":"meeting@example.com","passwordProfile":{"password":"change-me"},"e4a":{"userPurpose":"room"}}'
create users/car@example.com users '{"accountEnabled":true,"displayName":"Car Pool","mailNickname":"car","userPrincipalName":"car@example.com","passwordProfile":{"password":"change-me"},"e4a":{"userPurpose":"equipment"}}'
Some details:
- A user needs at least
accountEnabled,displayName,mailNickname,passwordProfileanduserPrincipalName.e4a.privilegeBitsis explained in Explanation ofprivilegeBits,e4a.maxSizein Listing mailbox sizes and setting quotas.e4a.userPurposeisuser,roomorequipment. - The
PATCHofverifiedDomainsreplaces the domains of the organisation. A domain missing from the list is taken out of it. - A domain with
"privilegeBits":1024(SUBROOT) can be the parent for subdomains. -
Upload a profile picture with a
PUTof the image to/api/v1/users/<user>/photo/$value:api 'http://manage-api/api/v1/users/markus@example.com/photo/$value' -X PUT \ -H 'content-type: image/jpeg' --data-binary @/storage/markus.jpgThe file has to be visible inside the container, for example in the data location.
-
Read an object back with a
GETof its path, and$selectto choose the fields, for examplehttp://manage-api/api/v1/e4a/aliases/example-alias.com?$select=id,e4a.
Use real passwords
The passwords in the script are placeholders. Generate one per user, and never keep a script with real passwords in the data location or in version control.
Permissions for using the Administration Interface¶
Permissions decide what each role may change. Every role includes everything the one above it can do:
| Role | Can |
|---|---|
| User | Log in, and view and change their own details, such as their password or profile picture. |
| Global Administrator | Delete domains, and view and create users, groups and aliases — but only within their own organisation. |
| System Administrator | View, create and delete organisations. |
Getting users from an LDAP source¶
From v6.0.8 the container can run ad-connect for you, so users come from a single directory. For how to configure ad-connect itself, see Managing users via Directory Synchronization.
Two things are needed:
- Store the configuration file at
/storage/adc.yaml. A sample sits next to it in/storage/adc.yaml.dist. - Pass the application id and secret token to the container in the
AD_CONNECT_AUTHenvironment variable.
With both in place, an e4a-ad-connect service starts inside the same container and handles password checks, and a cron job imports users every hour. To import immediately, run adc-import inside the container.
Connect clients and change passwords¶
Point your users at the End User Documentation, which covers every supported client.
Changing the Exchange4all configuration¶
Exchange4all reads one central YAML file, /storage/config.yaml, and propagates it to the individual services when the container starts. The defaults cover most deployments beyond the examples on this page. Do not change anything here unless support asks you to. Every available option and its default is listed in the container under /opt/exchange4all/config/conf.d/config-all-v1.yaml.
Changing PHP configuration¶
Requires version 8.0.0 or newer
Older containers ignore these variables.
Set any of these environment variables to change the PHP settings behind the web interface:
| Environment variable | Default | Description |
|---|---|---|
PHP_FPM_POOL_RPC_MAX_CHILDREN |
100 |
Maximum number of PHP-FPM worker processes for the RPC pool |
PHP_FPM_POOL_RPC_MAX_SPARE_SERVERS |
50 |
Maximum number of idle PHP-FPM processes allowed |
PHP_FPM_POOL_RPC_MEMORY_LIMIT |
32M |
PHP memory limit per process |
PHP_FPM_POOL_RPC_MIN_SPARE_SERVERS |
10 |
Minimum number of idle PHP-FPM processes |
PHP_FPM_POOL_RPC_POST_MAX_SIZE |
20M |
Maximum size of POST data (affects file uploads) |
PHP_FPM_POOL_RPC_START_SERVERS |
10 |
Number of PHP-FPM workers started on launch |
PHP_FPM_POOL_RPC_UPLOAD_MAX_FILESIZE |
20M |
Maximum file size for uploads via forms to RPC pool |
PHP_FPM_POOL_WWW_MAX_CHILDREN |
100 |
Maximum number of PHP-FPM worker processes for the frontend |
PHP_FPM_POOL_WWW_MAX_SPARE_SERVERS |
50 |
Maximum number of idle frontend PHP-FPM processes |
PHP_FPM_POOL_WWW_MEMORY_LIMIT |
64M |
Memory limit per frontend PHP process |
PHP_FPM_POOL_WWW_MIN_SPARE_SERVERS |
10 |
Minimum number of idle frontend processes |
PHP_FPM_POOL_WWW_POST_MAX_SIZE |
20M |
Maximum size of POST data for web interface (e.g. large attachments) |
PHP_FPM_POOL_WWW_START_SERVERS |
10 |
Initial number of frontend PHP processes on startup |
PHP_FPM_POOL_WWW_UPLOAD_MAX_FILESIZE |
20M |
Maximum upload size (e.g attachments) allowed in the web app |
The www pool serves clients such as the webmail client and ActiveSync devices; the rpc pool serves internal consumers such as the indexing component. As a rule of thumb, set memory_limit to three times the largest mail you expect: if your users may send 50MB, allow 150MB.
Installing a branding package¶
A branding package puts your own name, logos and colours on Exchange4all. Unlike changing each component on its own, the package is applied to all of them at once: the webmail client, the Manage console, the login page and the mails the system generates take their theme, logos and favicons from it. Every new version of the container applies the same package again, so your branding survives updates.
You receive the package from support as a tar.gz file. The package is signed, so do not change its files. Install it with the setup script:
./setup.sh --branding branding.tar.gz
The script checks that the archive is a branding package, unpacks it into /srv/exchange4all/branding and recreates the container, because the branding is applied when the container starts. Your users are disconnected until the container is healthy again, so the script asks first. A previous branding is kept as branding.bak-<date> next to it, the three newest are kept. On a deployment that was set up before, nothing else is changed. --check and --dry-run check the archive without installing it.
The container mounts the branding directory read-only as /branding. To keep it somewhere else, set DOCKERBRANDINGDIR in .env before you install the package. The default does not follow the data location you chose during setup.
Once the container is healthy, open the webmail client, the Manage console and the login page, and check that they show your branding. Send yourself a generated mail, such as a forgotten-password mail, to check the mails as well. Your browser may still show the old favicon from its cache.
If nothing changed, look for messages of 30_e4a-branding.sh in the container log:
docker compose logs exchange4all | grep 30_e4a-branding
Installing a branding package by hand
Unpack the package so that branding.yaml sits directly in the branding directory, then recreate the container:
mkdir -p /srv/exchange4all/branding
tar -xzf branding.tar.gz -C /srv/exchange4all/branding
ls /srv/exchange4all/branding/branding.yaml
docker compose up -d --force-recreate
If the archive unpacks into a subdirectory, move its content one level up. The services in the container do not run as root, so every file must be readable by all users.
To remove the branding, empty the directory and recreate the container with docker compose up -d --force-recreate. A plain restart keeps parts of the old branding in the container. Include the branding directory in your backups.
Required ports¶
Open these ports to the network:
| Service | Port(s) |
|---|---|
| Postfix | 25 (SMTP), 587 (submission, explicit TLS) |
| Web server | 80 (HTTP), 443 (HTTPS) |
| POP3 | 995 (TLS) |
| IMAP | 993 (TLS) |
Ports that must not be reachable¶
The container runs in host network mode, so every service inside it binds directly to your host's addresses. Alongside the ports above, these also listen on all interfaces and must not be reachable from the internet:
| Port(s) | Service |
|---|---|
| 110, 143 | POP3 and IMAP without TLS |
| 8025 | SMTP backend |
| 8080 | HTTP backend |
| 8110, 8143 | POP3 and IMAP backends |
A firewall is therefore not optional. The setup script looks for one and lists the exposed ports when it finds none. On a cloud provider, a security group permitting the same set of ports does the same job.
Allow SSH before enabling a firewall
Enable a firewall over SSH without permitting SSH first and you lock yourself out of the host. Allow it first:
ufw allow OpenSSH
ufw allow 25,80,443,587,993,995/tcp
ufw enable
ufw status
The equivalent with firewalld:
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-port=25/tcp --add-port=80/tcp --add-port=443/tcp \
--add-port=587/tcp --add-port=993/tcp --add-port=995/tcp
firewall-cmd --reload
Re-check an existing deployment with ./setup.sh --check. Against a running container it compares your firewall with the ports the host is really listening on.
Inspecting the log files¶
/var/log in the container is a link to /storage/log, so the log files survive a restart of the container. On the host they are in the e4a/log directory of your data location, /srv/exchange4all/e4a/log by default.
Every service supervised in the container sends its output to syslog-ng, tagged with the name of the service, for example e4a-imap or libregraph-lico. The startup scripts do the same, tagged with the name of the script. syslog-ng sorts the messages into files by facility and copies the content of /var/log/syslog to standard output, which is what docker compose logs shows. Two kinds of messages are not part of that output: mail and authentication. For those, look at mail.log and auth.log.
| File | What is logged there |
|---|---|
syslog |
All messages except mail, authentication and debug level. The same content as docker compose logs. |
user.log |
The output of the services and startup scripts. Mostly the same as syslog, without the messages of cron and syslog-ng itself. |
mail.log |
Postfix: incoming and outgoing mail, deliveries to e4a, rejects and queue handling. Also read by the Postfix metrics exporter. |
auth.log |
Logins and sessions, in practice the PAM sessions cron opens for its jobs. |
cron.log |
The cron daemon and which jobs it started. The output of the jobs themselves, such as lx-renew, lego-renew and ad-connect-import, goes to syslog. |
messages |
Informational messages from facilities that have no file of their own, mainly syslog-ng itself. |
debug |
Debug-level messages. Only created when something logs at that level. |
daemon.log, kern.log |
Kept from the default configuration. Nothing in the container usually writes there. |
php7.4-fpm.log |
The PHP-FPM master process: start, stop and crashed worker processes. |
A few services write to their own files instead of syslog:
| File | What is logged there |
|---|---|
apisix/apisix-error.log |
Warnings and errors of APISIX, the reverse proxy in front of all services. A failing backend shows up here first. |
apisix/http-access.log |
Every HTTP request reaching the container, with the backend it was routed to. |
apisix/stream-access.log |
Connections to the TCP services passed through by APISIX. |
caddy/webapp-access.log, caddy/webapp-error.log |
Requests to and errors of the webmail client. |
caddy/e4a-access.log |
Requests to the e4a HTTP scripts, such as EWS and the offline address book. |
caddy/manage-webapp-access.log, caddy/manage-api-access.log |
Requests to the administration interface and its API. |
caddy/caddy-access.log |
Requests for static files, the branding and the AD Connect download. |
e4a-servicerpc/servicerpc-grpc.log |
gRPC requests to the e4a service RPC. |
e4a-zcore/ewsrpc-grpc.log |
gRPC requests from the EWS component to zcore. |
The logs of the ActiveSync component are under /storage/push/logs, see Getting (debug) logs from the ActiveSync component.
The files are rotated inside the container. logrotate rotates syslog daily and keeps 7 files, the other syslog files weekly and keeps 4. APISIX starts a new file with a date prefix every day and keeps 56. Caddy rotates its files at 50 MB and keeps 20, for at most 90 days.
Older containers
Before the syslog-ng configuration was cleaned up, the container also wrote /var/log/error, which repeated all errors that were already in the other files, and copied the output of the services into messages as well. You may still find these files in an existing data location. They are no longer written and can be deleted.
Diagnosing a malfunction¶
When the deployment misbehaves, work through the following steps in order. They start at the container as a whole and end at the single service that is failing.
Check the health status of the container¶
Docker runs the container's own healthcheck.sh on a schedule, and docker compose ps shows you nothing but the result of its last run:
docker compose ps
To learn why a container is unhealthy, run that script yourself. It checks every service and prints the result of each one, so you get direct feedback in the terminal instead of a single word:
docker compose exec exchange4all healthcheck.sh --format documentation
Each failing check names the service behind it. That service is the one you continue with below.
Check whether a service was stopped¶
The services inside the container are supervised by runit and live in /etc/service/, which is the same directory as /etc/runit/runsvdir/default/. List their state with:
docker compose exec exchange4all sv status /etc/service/*
A service that crashed repeatedly is taken out of service, and a down file is written into its directory so it stays down across container restarts. Look for those files:
docker compose exec exchange4all ls /etc/service/*/down
A down file means the service did not fail once, it kept failing. Find the cause before you start it again — for e4a-delivery the usual one is a message stuck in the queue, see Inspecting and Managing the Postfix Message Queue. Once the cause is gone, remove the file and start the service:
docker compose exec exchange4all sh -c 'rm /etc/service/e4a-delivery/down && sv up e4a-delivery'
Read the container log for the reason of the crash¶
The services log to standard output, so the container log holds the error that made one of them crash:
docker compose logs --tail 500 exchange4all
Search it for the name of the failing service to get to the relevant lines, and keep in mind that the interesting message is usually the first error, not the last one. The supporting services that write to their own files are listed under Inspecting the log files.
Start the service in the foreground¶
If a service does not start at all, run its run script by hand. Everything it prints goes to your terminal, which is the fastest way to see a broken configuration file or a missing permission:
docker compose exec exchange4all sv down e4a-delivery
docker compose exec exchange4all /etc/service/e4a-delivery/run
Watch docker compose logs -f in a second terminal while you do this. Some startup errors are reported by other components of the container rather than by the service itself, and they only show up there. Stop the foreground process with Ctrl+C and hand the service back to the supervisor:
docker compose exec exchange4all sv up e4a-delivery
Turn on debug logging while you investigate
TESTING_LOGGING=true makes every e4a service log in detail, and LIBSEGFAULT_ENABLED=true gives you more information when a service crashes. See Enabling additional logging for debugging, and turn both off again afterwards.
Adding additional domains to the system¶
Your deployment is already reachable under the FQDN you gave the setup script. To serve a handful of further domains, list them directly so clients can auto-detect their configuration. If you host domains for external customers, use the adsredir service instead — see /etc/runit/runsvdir/default/e4a-service-adsredird/run inside the container.
To add domains:
- Open the
.envfile and extend the line starting withDOMAINS=, separating names with spaces inside the double quotes. - Recreate the container so it picks the new value up:
docker compose up -d. - Request a certificate that covers the new names and add it to the web server:
./setup.sh --renew-certificate.
Every name in DOMAINS must resolve to this host
If one of them does not, Let's Encrypt certificate retrieval and licence activation fail — and they fail for the whole list, not just the missing name.
Adding Additional Postfix Configuration¶
Override both main.cf and master.cf with a mechanism modelled on docker-mailserver:
- For
main.cf, create/storage/postfix-main-v2.cfin the same format asmain.cf. - For
master.cf, create/storage/postfix-master.cf. Every line is passed topostconf -P, so write your parameters as<service_name>/<type>/<parameter>.
Your changes apply the next time the container starts. To apply them immediately, run /etc/my_init.d/43_postfix-config-overrides.sh && sv restart postfix inside the container.
Pass all mails through a mail relay¶
Requires v7.2.0 or newer
To send both inbound and outbound traffic through a relay, the container runs Postfix as two instances. Enable it by adding POSTMULTI_ENABLED=true to the environment of the container.
Configure the relay and a unique hostname for the inbound instance in /storage/postfix-main-v2.cf:
myhostname=inbound.exchange4all.local
relayhost=[relay.example.com]:25
Then configure the outbound instance in /storage/postfix-outbound-main-v2.cf:
myhostname=outbound.exchange4all.local
relayhost=[relay.example.com]:25
inet_protocols=all
virtual_transport=smtp:[relay.example.com]:25
virtual_alias_maps=
mydestination=
alias_maps=
alias_database=
local_recipient_maps=
local_transport=error:5.1.1 Mailbox unavailable
Using a mail relay that requires auth¶
If your relay provider requires basic authentication, add three more lines to /storage/postfix-main-v2.cf, following Adding Additional Postfix Configuration:
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/storage/postfix/provider_auth
smtp_sasl_auth_enable turns basic authentication on. smtp_sasl_security_options states which mechanisms your provider accepts:
| Option | Description |
|---|---|
noanonymous |
Disallows anonymous authentication mechanisms (usually the default option) |
noplaintext |
Disallows mechanisms that transmit passwords in plain text |
noactive |
Disallows active authentication mechanisms (e.g. captcha) |
nodicationary |
Disallows mechanisms susceptible to dictionary attacks |
mutual_auth |
Only allows mechanisms that provide mutual authentication |
For the full list, see the Postfix documentation.
smtp_sasl_password_maps tells Postfix where your credentials are. The example keeps them in /storage/postfix/provider_auth, but the name and location are yours to choose — adjust the line if you use a different one.
Check which port your provider expects
Many relays require port 587 rather than 25. Set relayhost accordingly.
Running the setup script from anywhere¶
Whenever the setup script sets up or updates the deployment, it installs e4a-cmd into ~/.local/bin. It changes into the deployment directory and runs setup.sh there, so every option of the script works from any directory:
e4a-cmd --status
e4a-cmd --exec e4a userinfo user1@example.com
e4a-cmd --update
Run without options, e4a-cmd is the same as ./setup.sh --exec: it shows the status and opens a bash shell in the container.
Install it somewhere else with --cmd-dir, for example --cmd-dir /usr/local/bin to make it available to every user. The choice is stored as E4ACMDDIR in .env. When the directory is not in your PATH, the script tells you how to add it. With several deployments on one host, only the first one gets the command, an existing e4a-cmd of another deployment is left alone.
Checking the status and running commands¶
./setup.sh --status shows how the running deployment is doing, without changing anything: the container state with the result of its health checks, the image it runs compared with the version pinned in .env, the installed licences, the space left at the data location, how long the certificate served on port 443 remains valid, the clock, and files waiting to replace a locally changed one. It only looks at this host, the DNS and mail checks are left to --check.
It exits with 0 when everything is fine, 1 on warnings and 2 when the container is not running. For monitoring, --json prints the same report as a single JSON object:
./setup.sh --status --json | jq '.checks[] | select(.status != "ok")'
{"fqdn":"mail.example.com","status":"ok","checked_at":"2026-09-24T20:15:00Z","checks":[
{"name":"container","status":"ok","message":"running and healthy, up 3d 4h, 306 of 306 checks passed","state":"running","health":"healthy","uptime":"3d 4h","checks_total":306,"checks_failed":0},
{"name":"certificate","status":"ok","message":"valid for 75 more days, until 2026-12-09","days_left":75,"not_after":"2026-12-09"}
]}
./setup.sh --logs follows the output of the container. ./setup.sh --logs lnav opens the log files of the services in lnav instead, which merges them into one timeline and lets you filter and search. It picks the current file of each service, among them mail.log, syslog, the web server and APISIX access logs and the RPC services. For everything including rotated files, run lnav on the e4a/log directory of your data location yourself, /srv/exchange4all/e4a/log by default.
--exec runs a command in the container, or opens a bash shell when no command is given. Everything after --exec is the command, and the exit code is the one of the command. The status is printed first, on stderr, so the output of the command can still be piped. Add -q in front of --exec to leave it out:
# Get a bash shell in the container
./setup.sh --exec
# Get the information of the user user1@example.com
./setup.sh --exec e4a userinfo user1@example.com
# List every health check with its result, when the status reports a failed one
./setup.sh -q --exec healthcheck.sh --format documentation
# Show the version of the components inside the container
./setup.sh -q --exec cat /opt/exchange4all/.version
# Dump the database to a file on the host
./setup.sh -q --exec mysqldump email > email.sql
./setup.sh --exec is the same as docker compose exec exchange4all, run from the right directory. Every command of the cheat sheet works with it.
Listing mailbox sizes and setting quotas¶
The Manage API answers on a unix socket inside the container, without a login. It lists the users of a domain with the size of their mailbox and their quota:
e4a-cmd -q --exec curl -s -G --unix-socket /var/run/exchange4all-manage-api/rest0.sock \
http://manage-api/api/v1/users \
--data-urlencode "\$filter=endswith(userPrincipalName,'@example.com')" \
--data-urlencode "\$select=id,e4a" \
--data-urlencode "\$maxpagesize=25"
{"value":[{"id":"user1@example.com","e4a":{"status":0,"nickName":null,"usedSize":9623429,"maxSize":5368709120,"memo":null,"privilegeBits":7}}],"@odata.nextLink":"http://manage-api/api/v1/users?%24filter=...&%24maxpagesize=25&%24skipToken=yhFl-JIR"}
usedSize and maxSize are in bytes. usedSize adds up the items in the mailbox, which is what the quota is measured against. The mailbox takes more space on disk, because its database and indexes come on top. The filter is required: the API does not list the users of all domains at once.
The API returns the list in pages. Request no more than 25 items at a time with $maxpagesize. As long as there are more, the answer carries @odata.nextLink, the complete URL of the next page. Without $maxpagesize, a page holds 50 items. $top is no replacement: it only shortens the list and links to no further page.
This script follows those links. It prints every user of every domain with both values in MiB, and runs jq on the host:
#!/bin/sh
# Lists every user with the size of the mailbox and the quota, in MiB.
PAGE_SIZE=25
# items PATH: prints every item of a list as one line of JSON, page by page.
items() (
url="http://manage-api$1"
while [ -n "$url" ]; do
page=$(e4a-cmd -q --exec curl -s --unix-socket /var/run/exchange4all-manage-api/rest0.sock "$url" </dev/null)
printf '%s\n' "$page" | jq -c '.value[]'
url=$(printf '%s\n' "$page" | jq -r '."@odata.nextLink" // empty')
done
)
items "/api/v1/domains?\$maxpagesize=$PAGE_SIZE" | jq -r '.id' | while read -r domain; do
items "/api/v1/users?\$filter=endswith(userPrincipalName,'@$domain')&\$select=id,e4a&\$maxpagesize=$PAGE_SIZE" |
jq -r '[.id, (.e4a.usedSize / 1048576 | floor), (.e4a.maxSize / 1048576 | floor)] | @tsv'
done
In a loop, the input of e4a-cmd has to come from /dev/null. Otherwise docker compose exec reads the input of the loop, and the loop ends after the first page or domain. To list only the users who use more than 90% of their quota, replace the last jq with:
jq -r 'select(.e4a.maxSize > 0 and .e4a.usedSize > .e4a.maxSize * 0.9) | .id'
To change the quota of a user, send the new maxSize in bytes. This sets 10 GiB:
e4a-cmd -q --exec curl -s -X PATCH --unix-socket /var/run/exchange4all-manage-api/rest0.sock \
http://manage-api/api/v1/users/user1@example.com \
-H 'content-type: application/json' \
--data-raw '{"e4a":{"maxSize":10737418240}}'
A successful change prints nothing, an error is returned as JSON. The Manage console does not accept quotas below 1 GiB, and the API does not check that. Keep to the same minimum. For a single user, e4a userinfo shows the quota as well, as maximum size in MiB.
Managing mailbox folder permissions¶
e4a service admin mailbox-folder-permission reads and changes the permissions on the folders of any mailbox, without logging in as its owner. Use it to share a calendar or a folder tree on behalf of a user, to repair permissions after a migration, or to apply the same sharing to many mailboxes from a script. It talks to the local EWS endpoint and writes the same permissions Outlook sets in the folder properties.
The tool needs the experimental EWS operations. Enable them once by adding this line to .env and recreating the container with docker compose up -d:
E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS=true
Without it, the tool stops with EWS experimental operations are not enabled!.
Run it in the container, for example through e4a-cmd:
e4a-cmd --exec e4a service admin mailbox-folder-permission get user@example.com Calendar
| Command | What it does |
|---|---|
get <mailbox> [folder] |
Shows the permissions of a folder. Without a folder, the root of the mailbox. |
list <mailbox> [folder] |
Prints the paths of a folder and its sub-folders, 10 levels deep by default. Use it to find the exact path for the other commands. |
add <mailbox> <folder> <users> |
Grants permissions to users that have none on the folder yet. |
set <mailbox> <folder> <users> |
Changes the permissions of users that already have an entry on the folder. |
remove <mailbox> <folder> <user> |
Removes the entry of one user from the folder. |
<mailbox> is the address of the mailbox whose folders you change, <users> the users who get access to them.
Folder paths¶
Well-known folders can be given by name, independent of the language of the mailbox: Inbox, Calendar, Contacts, Tasks, Notes, Drafts, Sent, Deleted Items, Junk, Outbox, Archive and Journal. Case and spaces do not matter.
Every other folder is addressed by its path, with / between the levels, such as Inbox/Projects/2026. A slash that is part of a folder name is written as \/. The paths printed by list already use this form, so copy them from there:
e4a service admin mailbox-folder-permission list user@example.com Inbox --max-depth 2
Permission levels¶
Users are given as address:Level, several of them separated by commas: alice@example.com:Editor,bob@example.com:Reviewer. Besides addresses, Default stands for every authenticated user of the organisation and Anonymous for unauthenticated access.
The levels are the ones Outlook offers: Owner, PublishingEditor, Editor, PublishingAuthor, Author, NoneditingAuthor, Reviewer, Contributor and None. Calendars also accept FreeBusyTimeOnly and FreeBusyTimeAndSubjectAndLocation.
When none of them fits, Custom(...) sets the individual rights. Quote the argument, the parentheses would otherwise be read by the shell:
e4a service admin mailbox-folder-permission add user@example.com Calendar \
"assistant@example.com:Custom(edit:Owned,delete:Owned,read:FullDetails,create_items,folder_visible)"
| Field | Values |
|---|---|
edit, delete |
None, Owned, All |
read |
None, FullDetails, and on calendars TimeOnly or TimeAndSubjectAndLocation |
| Flags | create_items, create_subfolders, folder_owner, folder_visible, folder_contact |
Adding and changing permissions¶
add refuses a user that already has an entry on the folder and points you to set, and set refuses one that has none. This keeps a typo from silently replacing an existing permission. Two options of add relax that:
--allow-overridereplaces the entries of the given users and keeps all others.--clear-firstremoves all existing entries of the folder,DefaultandAnonymousincluded, before adding the given ones.
--clear-first replaces the whole permission list
Everyone not named on the command line loses access to the folder, including delegates the user set up themselves. Run get first and keep its output, so you can restore the previous state.
# Give the team read access to the calendar
e4a service admin mailbox-folder-permission add user@example.com Calendar team@example.com:Reviewer
# Raise it to Editor later
e4a service admin mailbox-folder-permission set user@example.com Calendar team@example.com:Editor
# Take it away again
e4a service admin mailbox-folder-permission remove user@example.com Calendar team@example.com
On success, the commands print nothing. Add --log-level info to see what was done, or --json for a result to process in a script. Both are global options and go before the command:
e4a service admin mailbox-folder-permission --log-level info add user@example.com Calendar team@example.com:Reviewer
Try changes with --dry-run first
add, set and remove accept --dry-run, which reports what would change without saving it. Combine it with --log-level info, especially for recursive changes.
Changing a folder tree¶
Permissions belong to a single folder, sub-folders do not inherit them. With --recursive, add, set and remove work on the folder and its sub-folders. --max-depth limits how deep: 1, the default, covers the direct sub-folders only, -1 the whole tree.
# Give the deputy access to the inbox and every folder below it
e4a service admin mailbox-folder-permission --log-level info add --recursive --max-depth -1 \
user@example.com Inbox deputy@example.com:Reviewer
A folder that does not fit the operation, because the user already has an entry for add or none for set and remove, is skipped and the others are processed. At the end it reports how many folders succeeded, were skipped and failed. The exit code is 1 only when a folder failed.
Rebuilding the search index¶
The indexer keeps a full-text index of every mailbox, which the search of the clients uses. New and changed items are indexed as they arrive. Rebuild the index of a mailbox when its search results are incomplete or contain items that no longer exist, for example after restoring the mailbox from a backup or after an import that ran with skip_indexer_reindex.
e4a service indexerd reindex queue add puts mailboxes on the reindex queue:
# Reindex two mailboxes
e4a-cmd --exec e4a service indexerd reindex queue add user1@example.com user2@example.com
# Reindex every active user
e4a-cmd --exec e4a service indexerd reindex queue add --all
# Every active user except two
e4a-cmd --exec e4a service indexerd reindex queue add --all --omit user1@example.com --omit user2@example.com
--all takes the active users from the database, disabled users are left out. It cannot be combined with addresses on the command line. --limit <n> stops after n users have been added. Addresses given on the command line are not checked, a typo only shows up as an error of the indexer later.
The command only queues the mailboxes and returns. The indexer service works through the queue in the background, eight mailboxes at a time. A mailbox that is already waiting or being reindexed is not added a second time.
--mode selects what is reindexed:
| Mode | What happens | When to use |
|---|---|---|
everything (default) |
Reads every item of the mailbox into the index again, removes entries of items that no longer exist and clears the list of failed items. | Search results are wrong or incomplete, the mailbox was restored. |
retry-previous-failures-only |
Only indexes the items that failed during earlier indexing. | The log reported failed items, for example after a temporary problem was fixed. |
Reindex all users outside working hours
A full reindex reads every item of every mailbox and puts noticeable load on the storage. Search results of a mailbox stay available while it is reindexed, but run --all when few users are working, or split it up with --limit and --omit.
Progress is logged to syslog with the tag e4a-service-indexer. Each mailbox logs reindex start, reindex ongoing every 1000 items, and finally reindex complete with the number of items, how many of them failed and how many missing items were removed from the index. reindex ended prematurely means the run was interrupted, for example by a restart of the container. The indexer remembers which mailboxes it was working on and starts them again from the beginning the next time it starts (reindex recover required), so there is nothing to queue again.
e4a-cmd -q --exec grep e4a-service-indexer /var/log/syslog | grep -E 'reindex (start|complete|ended)'
Rebuilding the IMAP index of a mailbox¶
IMAP and POP3 do not read the mailbox directly but through midb, which keeps its own index of the folders and messages of every mailbox. midb brings that index up to date when an IMAP session opens the mailbox. For a large mailbox that has never been opened over IMAP, for example right after a migration, this makes the first login slow, and clients may time out.
midb-sync updates the index of a mailbox ahead of time:
e4a-cmd --exec e4a service admin midb-sync user1@example.com
Run it after importing mailboxes whose users connect over IMAP, so they do not wait for the index on their first login. A loop over the migrated users does that in one go.
By default only folders that changed since the last update are read again. --force reads every folder, use it when IMAP clients show messages that no longer exist or miss messages that webmail and Outlook show:
e4a-cmd --exec e4a service admin midb-sync --force user1@example.com
Nothing happens while the mailbox is open over IMAP
If an IMAP or POP3 session currently has the mailbox open, midb keeps its index up to date anyway, and midb-sync reports success without doing anything. When --force seems to have no effect, ask the user to close their IMAP clients and run it again.
Resetting the webmail settings of a user¶
The webmail keeps the settings of each user, such as language, signatures, the reading pane and the column layout, as JSON in the mailbox. user-settings app reads and changes them without logging in as the user. Use it when a broken setting keeps the webmail from loading, or to prepare settings for many users from a script.
| Command | What it does |
|---|---|
user-settings app dump <user> |
Prints the stored settings as JSON. |
user-settings app clear <user> |
Deletes all settings. The webmail starts with its defaults at the next login. |
user-settings app patch <user> <path> <value> |
Changes one setting and leaves all others as they are. |
user-settings app set <user> <file> |
Replaces the settings with the content of a JSON file, - reads it from standard input. |
All of them run as e4a service admin user-settings app ... in the container.
clear and set replace all settings of the user
Signatures and every other personal preference of the webmail are lost. Run dump first and keep its output.
# Keep a copy of the settings on the host before changing anything
e4a-cmd -q --exec e4a service admin user-settings app dump user1@example.com > user1-settings.json
# Reset the webmail of a user to the defaults
e4a-cmd --exec e4a service admin user-settings app clear user1@example.com
patch takes the path of the setting with . between the levels, as it appears in the output of dump, and the new value as JSON. A string value therefore needs its own double quotes inside the shell quotes:
e4a-cmd --exec e4a service admin user-settings app patch user1@example.com settings.zarafa.v1.main.language '"de_DE.UTF-8"'
Missing levels of the path are created. patch needs existing settings, it fails with No settings found for the user. for a user that has never logged in to the webmail.
The output of dump holds two objects: the regular settings and the persistent settings of the webmail, each under the numeric MAPI property tag it is stored in. set expects only the regular settings object, not the whole output of dump. dump lists the regular settings first, so you can take them from a copy with jq:
jq 'to_entries[0].value' user1-settings.json | e4a-cmd -q --exec e4a service admin user-settings app set user1@example.com -
Moving a mailbox to another user¶
A mailbox is a directory, its maildir, which the database assigns to a user. To give the mailbox of one user to another, for example after a rename from jane.doe@example.com to jane.smith@example.com, move the content of the old maildir into the maildir of the new user.
- Disable both users, the old and the new one. Never change a mailbox on the file system while its user can log in.
-
Look up the maildir of both users.
e4a userinfoprints it:e4a-cmd -q --exec e4a userinfo jane.doe@example.comLooking up user by address "jane.doe@example.com"... ok username jane.doe@example.com maximum size 10240M maildir /opt/exchange4all/system/var/storage/u-data/m1/v3/1 created date 2026-06-16 00:00:00 +0000 UTC type 0 status 0In a script, read it from the database instead:
e4a-cmd -q --exec mysql -N -B email -e "select maildir from users where username='jane.doe@example.com'" -
Replace the maildir of the new user with the one of the old user. Run this inside the container, where the paths from step 2 are valid:
e4a-cmd --exec bash maildirold=/opt/exchange4all/system/var/storage/u-data/m1/v3/1 maildirnew=/opt/exchange4all/system/var/storage/u-data/m1/v5/1 rm -rf "$maildirnew" mv "$maildirold" "$maildirnew" -
Remove the references to the old address from the mailbox, see Resetting the special folders after restoring a mailbox:
e4a-cmd --exec e4a service admin reset-store-user jane.smith@example.com -
Enable the new user. After the next login, they see their old mailbox.
rm -rf deletes the mailbox of the new user
Everything in the maildir of the new user is gone after step 3. Check both paths twice, and take a backup first.
Do not change the maildir in the database instead
Pointing the new user at the old maildir in the database looks quicker, but every alias of the user carries the maildir as well. Missing one breaks the mailbox in ways that are harder to repair than a mv.
What does not move with the mailbox:
- Client profiles. The username changed, so Outlook, ActiveSync devices and IMAP clients have to be set up again.
- Permissions in other mailboxes. A folder another user shared with the old address has to be shared with the new address again, see Managing mailbox folder permissions.
- Group memberships. Add the new user to the groups of the old one.
Renaming a user that comes from LDAP
When the user is renamed in LDAP, ad-connect no longer finds the old address and disables that user. Deleting it straight away can be configured instead. It also finds the new address and creates that user, optionally disabled. Both happen in no fixed order. Make sure both users are disabled before you move the maildir, and only enable the new user afterwards.
Copying mailboxes to a testing system¶
A testing system finds data related bugs best with real mailboxes. Copy the maildir of a production user into the maildir of a user on the testing system. Both systems should run the same version, or the testing system a newer one.
- On the testing system, create the user and disable it.
- Look up the maildir of the production user and of the testing user, as in Moving a mailbox to another user. Inside the container,
/opt/exchange4all/system/var/storageise4a/system/var/storagein the data location on the host,/srv/exchange4all/e4a/system/var/storageby default. -
Copy the maildir from a snapshot of production rather than from the live directory, which keeps changing while you copy. Keep the numeric owners:
rsync -a --numeric-ids --delete \ /tank/exchange4all/.zfs/snapshot/2026-10-04/e4a/system/var/storage/u-data/m1/v3/1/ \ test-host:/srv/exchange4all/e4a/system/var/storage/u-data/m1/v7/1/ -
On the testing system, reset the store user, rebuild the search index and the IMAP index, then enable the user:
e4a-cmd --exec e4a service admin reset-store-user test1@test.example.com e4a-cmd --exec e4a service indexerd reindex queue add test1@test.example.com e4a-cmd --exec e4a service admin midb-sync test1@test.example.com
The same steps copy a mailbox between two users on one host. Use cp -a instead of mv in Moving a mailbox to another user, so the original stays in place.
Copies contain production data
The testing system holds real mail of real users. Protect it like production, and do not let it send mail to the outside, for example by sending everything through a mail relay that discards it.
Resetting the special folders after restoring a mailbox¶
The root folder and the inbox of a mailbox store references to special folders that Outlook creates, among them Conflicts, Sync Issues, Local Failures, Server Failures and Junk E-mail. These references contain the identity of the mailbox they were created in. When you restore a mailbox into a different user, they still point to the old mailbox, and Outlook fails to open or sync these folders.
reset-store-user removes the references, and Outlook registers the special folders again on its next start:
e4a-cmd --exec e4a service admin reset-store-user user1@example.com
Run it once after restoring a mailbox into a user other than the one it was taken from. The folders and their content stay where they are, only the references are removed. For a mailbox restored into the same user, it is not needed.
Repairing mailboxes migrated from Kopano¶
Mailboxes migrated from Kopano can contain properties that Exchange4all does not need and that confuse Outlook. Typical symptoms are a folder list that Classic Outlook never finishes syncing, appointments that trip up Classic Outlook, and attached messages that fail to open. clear-unregistered-named-properties cleans up the database of such mailboxes. Despite its name, it does more than handle unregistered named properties. In each mailbox it:
- removes internal row IDs that were stored as properties,
- removes non-transmittable properties from the ranges reserved for them, except those Exchange4all uses,
- removes all named properties from folders,
- removes the stored parent folder name from embedded messages, which have no parent folder,
- gives appointments in the default calendar a message status when they have none,
- registers named properties that are used but missing from the registry of the mailbox, under a placeholder name.
# Show what would be changed in one mailbox
e4a-cmd --exec e4a service admin clear-unregistered-named-properties --dry-run user1@example.com
# Clean one mailbox
e4a-cmd --exec e4a service admin clear-unregistered-named-properties user1@example.com
# Clean every mailbox of organisation 5
e4a-cmd --exec e4a service admin clear-unregistered-named-properties --all --organization-id=5
# Clean every mailbox on the system
e4a-cmd --exec e4a service admin clear-unregistered-named-properties --all
Options take their value after =, as in --organization-id=5. --all includes disabled users. --maildir takes the path of a mailbox directory instead of a user, for a mailbox that no user points to.
For each mailbox, the tool prints Processing: <mailbox directory>, followed by one line for every kind of change it made. A mailbox without further lines needed no change. With --dry-run, the changes are counted and then rolled back.
The tool changes the mailbox database directly
It edits exmdb/exchange.sqlite3 of every mailbox it processes, in one transaction per mailbox. Take a backup first, run it with --dry-run before the real run, and run it when the users are not working, ideally right after the migration before they connect their clients.
The removed rows are copied into backup tables inside the same database, named after the original table with _cnp_, _cfnp_ or _cepd_ and a Unix timestamp appended, for example folder_properties_cfnp_1790000000. Only tables that received rows are kept. They stay in the database until you remove them, and are your way back if a removed property turns out to be needed. The internal row IDs and the added message status are not backed up, both can be recreated.
The tool is safe to run again: a second run finds nothing left to change.
Diagnostic tools¶
Two more tools only read data. They are mostly useful when you work on a problem together with support. When IMAP clients show different messages than webmail and Outlook, see Rebuilding the IMAP index of a mailbox instead.
entryid-to-json decodes a MAPI entry ID, as it appears in logs, in Outlook diagnostics or in the output of other tools, hex or base64 encoded. It shows which kind of object the ID refers to, and for folders and messages of a mailbox their internal folder and message ID:
e4a-cmd -q --exec e4a service admin entryid-to-json <entryid>
dac-query lists the additional data tables that services keep in a mailbox outside its folders. Each level of the command narrows it down, and the rows are printed as one JSON object per line:
# The namespaces of the mailbox
e4a-cmd -q --exec e4a service admin dac-query namespaces user1@example.com
# The tables in one namespace
e4a-cmd -q --exec e4a service admin dac-query tables user1@example.com <namespace>
# The rows of one table, 100 by default
e4a-cmd -q --exec e4a service admin dac-query table --limit=20 --start=0 user1@example.com <namespace> <table>
Updating¶
The recommended way to update is to re-run the setup script with --update. It looks up the newest version for the configured variant, pins it by digest, and recreates the container:
./setup.sh --update
An update asks no questions. Every setting is taken from the existing .env file and only the version changes, so it is safe to run unattended. Individual settings can still be changed in the same run by adding the matching option, for example ./setup.sh --update --acme production.
The setup script can update itself as well. The checks warn when a different version is published, and --self-update replaces the script after verifying it against the published checksum. The previous script is kept as setup.sh.bak. Combined with other options, the new script runs with them right away:
./setup.sh --self-update --update
Alternatively the E4AVERSION value in the .env file can be replaced by hand with a version from the release notes, and the container recreated afterwards. A version that is already pinned is never changed without --update.
Beta releases are skipped by updates
Releases with a minor version of 0, such as 8.0.4, are general beta releases. --update never resolves them, so a deployment only moves to x.1.y and later versions. To run a beta deliberately, pin it explicitly:
./setup.sh --image-version 9.0.1@sha256:...
Prometheus Metrics¶
Metrics are served on port 9100, which your firewall blocks by default. Open it for your monitoring host only.
With ufw:
ufw allow from 198.51.100.10 to any port 9100
ufw status
The equivalent with firewalld:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
source address="198.51.100.10" port port="9100" protocol="tcp" accept'
firewall-cmd --reload
firewall-cmd --list-rich-rules
Replace 198.51.100.10 with the address of your monitoring host, then scrape http://your-server.com:9100/metrics.
Enabling/disabling additional plugins on startup¶
Exchange4all uses plugins internally for functionality such as user limit enforcement in hosting setups. Plugins live in the container filesystem, so every change to them is lost when you recreate the container. Set them in the environment instead and they are reapplied on every start:
# list of additional plugins to enable
E4A_FORCE_ENPLUGIN="zcore:svc:oidc delivery:svc:remote_mx_hello"
# List of additional plugins to disable
E4A_FORCE_DISPLUGIN="smtp:pas:status_header delivery:svc:remote_mx_hello"
Only set these when support asks you to
Enabling or disabling the wrong plugin can break mail flow in ways that are hard to diagnose.
Backup¶
Exchange4all supports full system backups. You can extract individual users and mailboxes from such a backup, but restoring a single folder or item is not part of the software. Take file system snapshots — our reference system uses zfs snapshots.
What to back up¶
| On the host | Inside the container | Back it up? |
|---|---|---|
.env and docker-compose.override.yml in the deployment directory |
— | Yes. They hold the configuration your deployment was built with. A data directory without them is significantly harder to bring back up. |
/srv/exchange4all/e4a |
/storage |
Yes. Service configuration, generated passwords, licences, certificates, the Redis database and the user data under /storage/system/var/storage. Redis writes an append-only file, so it can be snapshotted as it is. /storage/log and /storage/coredumps can grow large and may be left out. |
/srv/exchange4all/db |
/var/lib/mysql |
Yes, at the same moment as /storage, see below. The MariaDB database holds the system data: users, organisations and domains. Mails are not stored in it. MariaDB runs on InnoDB. |
/srv/exchange4all/php |
/var/lib/php/sessions |
Optional. Losing the sessions only means your users log in to the webmail client again. |
/srv/exchange4all/branding |
/branding |
If you use your own branding. |
Docker volumes e4a_postfix-spool and e4a_postfix-outbound-spool |
/var/spool/postfix, /var/spool/postfix-outbound |
Optional. Mail that is still queued for delivery. The prefix is COMPOSE_PROJECT_NAME from .env. |
/run/exchange4all/webapp |
/storage/webapp/tmp |
No. Short-lived attachment data of the webmail client, mount it as tmpfs on the host. |
Taking a consistent snapshot¶
The mailboxes in /storage belong to the users, organisations and domains in the database, so both should be captured at the same moment. Restored from different points in time, a user created in between exists in one and not in the other: a mailbox without its user, or a user without a mailbox.
The preferred way is a zfs snapshot. Keep the data location on one dataset, or take a recursive snapshot, which is atomic across all child datasets:
zfs snapshot -r tank/exchange4all@$(date +%F)
Dumping the database¶
A dump can be written straight to the host:
./setup.sh -q --exec mysqldump email | gzip > email-$(date +%F).sql.gz
backup-mariadb.sh does the same inside the data location and keeps the dumps of the last eight days. It writes them to /storage/backup/<date>/mysql/email.sql.gz, so they become part of every snapshot of /storage. Place it in the data location once, then run it through the setup script:
curl -fsSL -o /srv/exchange4all/e4a/backup-mariadb.sh https://manual.myexchange.rocks/on-premise/backup-mariadb.sh
./setup.sh -q --exec bash /storage/backup-mariadb.sh
To run it every night before your snapshot or backup job, add a line like this to the crontab of root. The setup script changes into its own directory, so the path is all it needs:
15 2 * * * /opt/exchange4all/setup.sh -q --exec bash /storage/backup-mariadb.sh
Restoring¶
To restore a whole deployment, on the same or on a new host:
- Stop the container with
docker compose down. Never add-v, it deletes the mail queue volumes. - Restore the data location,
.envanddocker-compose.override.yml, all from the same snapshot. - Start it again with
docker compose up -d, or on a new host run./setup.sh -y, which takes every setting from the restored.env. - Confirm the result with
./setup.sh --status.
To restore only the database from a dump, into a running container:
gunzip -c email-2026-09-24.sql.gz | ./setup.sh -q --exec mysql email
mysqldump drops and recreates every table, so this replaces all users, organisations and domains with the state of the dump. Changes made since then are lost, and mailboxes of users created since then are left without their user.
Deploying for production¶
Work through these lists before you put a deployment in front of real users.
Testing system¶
- A separate testing system runs next to production, ideally on the
-nextvariant all the time. It holds copies of some of your production mailboxes, so bugs that only show with real data surface there before they reach production. If a-nextbuild causes problems, switch back to the regular build of the same version.
Host and network¶
./setup.sh --checkreports no failures, and no warnings you have not consciously accepted.- Your firewall exposes only the required ports, for IPv4 and IPv6 alike. Port
9100for Prometheus metrics is open to your monitoring host only. - Certificates come from the Let's Encrypt production CA, not from
stagingor the container's own CA. - Your licence is installed and activated. Someone reads the mailbox of your System Administrators, because the licence expiry reminders go there.
- Core dumps are configured, ideally on a separate mount or volume.
- Monitoring and log ingestion are in place. The container has a built-in health check you can watch, see Diagnosing a malfunction. Never let the server run out of disk space — data lost that way cannot be fully recovered.
Mail delivery¶
- A separate mail gateway handles anti-spam, anti-virus and DKIM signing.
- Every address that sends mail has a reverse DNS (PTR) record. It points to the hostname Postfix announces, and that hostname resolves back to the same address. Large mail providers reject or file as spam mail from addresses without one.
- Every mail domain publishes MX, SPF and DMARC records, see Configuring defaults for MX, SPF and Autodiscover.
- Your hosting provider allows outgoing connections on port 25. Many cloud providers block it by default. If yours does, ask them to lift the block or send through a mail relay.
- Your IP addresses are not on a public blocklist. Addresses handed out by cloud providers often carry a history.
Users and sizing¶
- You know which clients your users will connect with, and each of them is set up to find the server:
- Outlook needs Autodiscover, see Configuring defaults for MX, SPF and Autodiscover.
- Mail programs using IMAP and SMTP find the server through the SRV records for client autodiscovery.
- Point your users at the End User Documentation for the setup of each client.
- The rate limits suit your users. If many of them share one IP address, such as an office behind NAT, raise the limit for that address. Otherwise they lock each other out on the first day.
- The outgoing mail quotas suit how much mail your users send. Mail over a quota is rejected and bounces back to the sender. Raise the limits for users or domains that send newsletters or notifications. Sending rate limits explains the quotas to your administrators; if you change the defaults, tell them the new values.
- The settings match the number of users and connections you expect:
- The PHP settings allow enough worker processes for your webmail and ActiveSync users, and
memory_limitand the upload limits fit the largest mail you allow. - The open file limits in the optional environment variables cover the connections you expect.
- For the settings of the Exchange4all components in
/storage/config.yaml, tell support how many users and connections you expect before you change anything.
- The PHP settings allow enough worker processes for your webmail and ActiveSync users, and
Operations¶
- Your backups cover the data directory and the
.envfile, see Backup. Database and data directory come from the same snapshot, and a copy is kept off the host. - You have restored a backup once, ideally on a separate host, and confirmed that it works.
- Nothing is left over from testing:
- No preview image is running.
E4A_IP_FILTER_LOG_ONLYandE4A_SESSION_RATE_LIMIT_LOG_ONLYare not set.E4A_FORCE_ENPLUGINandE4A_FORCE_DISPLUGINare only set when support asked for it.- Debug logging is turned off.
- The generated admin password from
/storage/admin.secretis changed or kept in a password manager. A second System Administrator exists, so one lost password does not lock you out. - Someone follows the release notes and updates the deployment. Take a snapshot before every update.
Running test/preview versions¶
Preview builds are published as -next images to the same registry as the regular ones, so no
additional access is needed beyond the docker login described above. To switch a deployment to them, re-run the setup script and select the preview variant:
./setup.sh --variant next --update
That resolves the newest -next tag, pins it by digest and recreates the container. Switch back the same way with --variant full or --variant slim.
Every -next build has a regular build with the same version number, for example 8.7.4-next and 8.7.4. --update moves you to the newest regular version, which may already be newer. To go back to the regular build of the version you are running, pin it instead. The digests are listed on Container images:
./setup.sh --variant full --image-version 8.7.4@sha256:...
To debug one specific problem, pin the image support gives you:
./setup.sh --image-version test-image-for-problem@sha256:...
Preview builds are not supported for production use
--update never selects them on its own. Always start a preview container with a specific digest, as shown above.
Security¶
The container defends itself against abuse out of the box. Tune the limits below to your user base rather than removing them.
Rate limiting incoming requests¶
The defaults suit hosting scenarios where few users share an IP address. If a larger group of your users comes from one address — an office behind NAT, for example — either raise the limit for that address or adjust the defaults.
Every service that handles user requests has its own IP filter. The http section covers Outlook, ActiveSync clients, EWS and the webmail client:
e4a:
http:
ip_filter:
audit_interval: 1second
audit_times: 200
imap:
ip_filter:
audit_interval: 1second
audit_times: 200
pop3:
ip_filter:
audit_interval: 1second
audit_times: 200
smtp:
ip_filter:
audit_interval: 1second
audit_times: 200
audit_times is how many requests are allowed within audit_interval. Write the interval as Xsecond, for example 5second or 60second; 0second disables the filter. Each service prints its current rate limiting configuration on startup, so you can confirm your changes took effect.
To give individual addresses their own limits, list them in /storage/system/var/data/ip_filter.txt:
# ip audit_times audit_interval
127.0.0.1 1 0second # rate limiting is disabled for this ip
198.51.100.5 400 1second # allows 400 requests every second
203.0.113.19 1000 5second # allows 1000 requests every five seconds
Rate limiting user sessions¶
After the IP filter, the container limits how many sessions a single user may create in a period. This one restricts the user, not the address they come from.
e4a:
common:
session:
user_session:
rate_limit_period: 10minutes
rate_limit_burst: 100
rate_limit_burst is how many new login sessions a user may create within rate_limit_period.
Disabling connection and session limits
Log-only mode keeps the checks running but stops them rejecting anything, which is useful while you diagnose a limit that fires too early. Do not leave a production system in this state.
E4A_IP_FILTER_LOG_ONLY=1stops the IP filter from enforcing.E4A_SESSION_RATE_LIMIT_LOG_ONLY=1stops the session rate limit from enforcing.
Querying active sessions
Sessions live in Redis. Dump them as JSON with:
e4a service admin dump-sessions
To see the sessions of one user:
e4a service admin dump-sessions | jq 'select(.record[1].v1.username == "username@example.com")'
Rate limiting outgoing emails¶
The e4a-policyd service manages outgoing email quotas as leaky buckets:
- Every
interval, each bucket is refilled to its limit. - Each message sent takes from the bucket.
- When the bucket is empty, no more messages go out until the next refill.
Set the refill interval and the bucket sizes in /storage/config.yaml. The defaults are:
e4a:
policyd:
quota:
smtp:
key_prefix: "pol:quota:"
interval: 12hours
limits:
default: 500
org: 1000
domain: 1000
internal: 1000
| Bucket | Applies to |
|---|---|
default |
Each individual user. |
internal |
Mail sent to recipients on the same server. |
domain |
Everything sent by the sending user's domain. |
org |
Everything sent by the user's organisation, on top of the domain limit. |
Limiting connections at our reverse proxy (Apisix)¶
From version 8.8.0 the container loads the APISIX limit-count and limit-conn plugins, so you can tighten connection limits temporarily on top of the rate limits above:
limit-countcaps how many requests a key, such as an IP address, may make within a time window.limit-conncaps how many connections that key may hold open at once.
Neither is applied to a route by default. Add one to the plugins section of the route you want to protect. This caps concurrent connections to the webmail app per IP address:
routes:
- name: E4A App
...
plugins:
limit-conn:
conn: 200
burst: 100
default_conn_delay: 0.1
key_type: var
key: remote_addr
rejected_code: 503
Testing changes before making them persistent
Edit /run/apisix/apisix-routes.yaml first. Changes there apply immediately without a restart, and are lost on the next one. Once the values do what you want, make the same edit in /storage/apisix-routes.yaml, which is copied over /run/apisix/apisix-routes.yaml on every container start.
Frequently asked questions¶
How does licence activation work?¶
Activation uses the ACME protocol to retrieve a short-lived certificate that unlocks the features of your subscription. The licence exchange tool, lx, performs an HTTP-01 challenge for every domain in the DOMAINS environment variable — which is why all of them have to resolve to your host.
Those certificates are valid for 60 days, and lx renews them automatically once they have less than 40 days left.
How do I reset the admin password?¶
If you lost the main admin password and have no other system administrator who could reset it from the Manage console, send yourself a forgotten-password mail:
docker compose exec exchange4all \
curl -s --unix-socket /var/run/exchange4all-manage-api/rest0.sock "http://manage-api/api/v1/invitations" \
-H 'content-type: application/json' \
--data-raw '{"invitedUserDisplayName":"Administrator","invitedUserEmailAddress":"external-address@example.com","inviteRedeemUrl":"https://exchange4all.local/manage/#/oidc/request-callback?to=set-password","sendInvitationMessage":true,"invitedUserType":"Member","invitedUser":[{"id":"admin@exchange4all.local"}]}'
Replace invitedUserEmailAddress and id with the details of the user you are recovering.
If your container cannot send mail yet, set the password directly instead:
docker compose exec exchange4all \
curl -s --unix-socket /var/run/exchange4all-manage-api/rest0.sock "http://manage-api/api/v1/users/admin@exchange4all.local" \
-X 'PATCH' \
-H 'content-type: application/json' \
--data-raw '{"passwordProfile":{"password":"new-password"}}'
Replace admin@exchange4all.local with the id of the user whose password you are changing.
How does automatic configuration of Outlook and mobile clients work?¶
Microsoft documents the steps at Autodiscover services in Outlook.
Why does Exchange4all need a dedicated IP address?¶
The container terminates SSL itself and does not support an SSL-terminating proxy in front of it. If you use the adsredir service, you need a second dedicated IP.
Why does Exchange4all only configure smtp authentication on the inbound port?¶
Authentication is allowed on the submission port (587) and not on the SMTP port (25). Keeping client-to-server traffic separate from server-to-server traffic lets you rate limit the two independently.
What if Outlook Autodiscover detects IMAP?¶
During Simplified Account Creation, Outlook asks Microsoft's service at prod-autodetect.outlookmobile.com. After a provider change that service can still return cached values for the old provider. When that happens, choose "Configure manually" and then "Exchange", and Outlook detects the remaining settings correctly.
To skip that manual step on your users' machines, set either of these registry values:
HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Outlook\Setup\DisableAccountSettingsDetectionServiceas aDWORDof1disables the detection service, so Outlook goes straight to the account type dialogue where your user picks "Exchange". This reg file sets it for you.HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Outlook\Setup\DisableOffice365SimplifiedAccountCreationas aDWORDof1disables simplified account creation altogether, and with it the lookup against Microsoft.
I am getting midb out of memory during a migration, even though I have plenty of ram left¶
Large IMAP migrations can fill the internal buffers of midb, which reports midb out of memory even with free RAM on the host. Raise these limits for the duration of the migration:
e4a:
imap:
context_average_mem: 512K
context_max_mem: 5M
After the migration, run midb-sync for the migrated users, so their first IMAP login does not have to build the index.
How do disabled users/domains affect usage reporting?¶
Disabling a user or a domain deducts it from your licensed user count immediately. Mail for those addresses is no longer accepted.
Why are my users getting "Sorry, this address is not allowed." when trying to create users?¶
The requested address contains a word from the system-wide "bad words" list, which exists to keep inappropriate addresses out of your deployment.
Either have the user choose a different name, or create the account from an account with system administrator privileges — those bypass the naming policy checks.
Appendix¶
Docker Compose Cheat Sheet¶
Commands you will reach for most often:
# Show the health, image, licences, storage and certificate of the running deployment
./setup.sh --status
# Re-run the host checks against a running deployment, without changing anything
./setup.sh --check
# Move to the newest version of the configured variant and recreate the container
./setup.sh --update
# Update the setup script itself
./setup.sh --self-update
# Install a branding package and recreate the container to apply it
./setup.sh --branding branding.tar.gz
# Get a bash shell in the container, after a short status report
./setup.sh --exec
# Request a new certificate for every name in DOMAINS and apply it
./setup.sh --renew-certificate
# Follow the container output, or open the service log files in lnav
./setup.sh --logs
./setup.sh --logs lnav
# Show the version of the components inside the container
docker compose exec exchange4all cat /opt/exchange4all/.version
# Pull the container tag specified in .env (or the latest if nothing is specified)
docker compose pull
# Start the container in the background, then put the container output into a new command
docker compose up -d && docker compose logs -f
# Get a bash shell in the container
docker compose exec exchange4all bash
# Get a list of environment variables from the container
docker compose exec exchange4all env | sort
# Stop and remove the container. Data in the storage directory is kept.
# Never add -v here, it also deletes the mail queue volumes.
docker compose down
# restart the container (will restart but not rebuild)
docker compose restart
# get verbose healthcheck information from the container
docker compose exec exchange4all healthcheck.sh --format documentation
# get the information of the user user1@exchange4all.local
docker compose exec exchange4all e4a userinfo user1@exchange4all.local
# reset the special folder references of user1@exchange4all.local after restoring the mailbox to a new user, see Resetting the special folders after restoring a mailbox
docker compose exec exchange4all e4a service admin reset-store-user user1@exchange4all.local
# display the current webapp settings of user1@exchange4all.local as a json object
docker compose exec exchange4all e4a service admin user-settings app dump user1@exchange4all.local
# clear the webapp settings for user1@exchange4all.local, see Resetting the webmail settings of a user
docker compose exec exchange4all e4a service admin user-settings app clear user1@exchange4all.local
# update the IMAP index of user1@exchange4all.local ahead of the first IMAP login, see Rebuilding the IMAP index of a mailbox
docker compose exec exchange4all e4a service admin midb-sync user1@exchange4all.local
# dump the active login sessions as JSON, see Rate limiting user sessions
docker compose exec exchange4all e4a service admin dump-sessions
# show only the sessions of user1@exchange4all.local (jq runs on the host)
docker compose exec -T exchange4all e4a service admin dump-sessions | jq 'select(.record[1].v1.username == "user1@exchange4all.local")'
# If the systemd unit was installed, this restarts the container
systemctl restart exchange4all
Files written by the setup script¶
setup.sh creates these files for you — they are reproduced here for reference, and you never need to download them separately.
The generated docker-compose.yml
# Generated by setup.sh 6 -- do not edit.
# Local changes belong into docker-compose.override.yml.
# See https://manual.myexchange.rocks/on-premise/
# managed-sha256: fb863dd036dde6a962c60520d1358ae36a107b517d04c0e6797011468eb277b9
services:
exchange4all:
image: ${E4AIMAGE:-registry.kopano.cloud/e4a-container}:${E4AVERSION:-latest}
hostname: ${E4AFQDN:-exchange4all.local}
network_mode: host
restart: unless-stopped
read_only: false
environment:
- E4AFQDN=${E4AFQDN:-exchange4all.local}
- E4AMAILDOMAIN=${E4AMAILDOMAIN:-exchange4all.local}
- LEEMAIL=${LEEMAIL:-}
- ACMESERVER=${ACMESERVER:-staging}
- DOMAINS=${DOMAINS:-} # additional domains for Let's Encrypt certificate
- E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS=${E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS:-false}
volumes:
- /var/lib/dbus/machine-id:/var/lib/dbus/machine-id:ro
- ${DOCKERBRANDINGDIR:-/srv/exchange4all/branding/}:/branding:ro
- ${DOCKERSTORAGEDIR:-/srv/exchange4all}/db/:/var/lib/mysql
- ${DOCKERSTORAGEDIR:-/srv/exchange4all}/e4a/:/storage
- ${DOCKERSTORAGEDIR:-/srv/exchange4all}/php/:/var/lib/php/sessions
- ${DOCKERTMPDIR:-/run/exchange4all}/webapp:/storage/webapp/tmp
- postfix-spool:/var/spool/postfix
- postfix-outbound-spool:/var/spool/postfix-outbound
tmpfs:
- /run
- /tmp
ulimits:
nofile: 1024000
stop_grace_period: 1m30s
logging:
options:
max-size: ${DOCKERLOGGING_MAXSIZE:-10m}
max-file: ${DOCKERLOGGING_MAXFILE:-3}
volumes:
postfix-spool:
postfix-outbound-spool:
The generated .env
# Configuration of the Exchange4all container.
#
# This file is read by docker compose. It is created and updated by setup.sh, but can also be
# edited by hand. Recreate the container after a change: docker compose up -d
#
# Keep this file in your backups, together with the data directory.
# The name this deployment is reached at. Certificates are requested for it.
E4AFQDN="exchange4all.local"
# First mail domain to create. Only relevant on the very first start.
E4AMAILDOMAIN="exchange4all.local"
# Every name a certificate should cover, separated by spaces. All of them must resolve here.
DOMAINS="exchange4all.local"
# Leave empty to use the container's own CA. Set an address to use Let's Encrypt, which means
# you agree to the Let's Encrypt terms of service.
LEEMAIL=""
# staging while testing certificate retrieval, production once it works.
ACMESERVER="staging"
# Container image and the exact version to run. Update with: ./setup.sh --update
E4AIMAGE="registry.kopano.cloud/e4a-container"
E4AVERSION=""
# full, slim or next. Decides which tag ./setup.sh --update resolves to.
E4AVARIANT="full"
# Where Exchange4all stores its data on this host.
DOCKERSTORAGEDIR="/srv/exchange4all"
# Short lived webmail attachment data. Mounting this as tmpfs on the host is recommended.
DOCKERTMPDIR="/run/exchange4all"
# Names the container and its volumes. Changing this orphans an existing deployment.
COMPOSE_PROJECT_NAME="e4a"
# Where setup.sh installs e4a-cmd, which runs it from anywhere. Empty for $HOME/.local/bin.
E4ACMDDIR=""
# Written by setup.sh so it can migrate older installations.
SETUPVERSION=""
# Optional settings, see https://manual.myexchange.rocks/on-premise/
# Experimental EWS operations. Disabled unless this is set to true.
#E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS="true"
# Where the branding package lives. Install one with: ./setup.sh --branding branding.tar.gz
#DOCKERBRANDINGDIR="/srv/exchange4all/branding"
#AD_CONNECT_AUTH=""
#POSTMULTI_ENABLED="true"
#PHP_FPM_POOL_WWW_MEMORY_LIMIT="64M"
#TESTING_LOGGING="true"
The optional systemd unit
On installation, WorkingDirectory and the compose command are replaced with the real values of your deployment.
[Unit]
Description=Exchange4all
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/exchange4all
TimeoutStartSec=15min
ExecStart=docker compose up -d --remove-orphans
ExecStop=docker compose down --remove-orphans
[Install]
WantedBy=multi-user.target
Additional DNS Entries for Client Autodiscovery¶
RFC 6186 defines DNS entries that let clients discover your hostnames and ports. Publish them as follows:
# grep SRV zones/my.server/my.server.zone
_imap._tcp IN SRV 0 0 0 .
_imaps._tcp IN SRV 0 0 0 993 your-server.com.
_pop3._tcp IN SRV 0 0 0 .
_pop3s._tcp IN SRV 10 0 995 your-server.com.
_smtp._tcp IN SRV 0 0 0 0 .
_smtps._tcp IN SRV 0 0 0 .
_submission._tcp IN SRV 0 0 0 587 your-server.com.
_autodiscover._tcp IN SRV 0 0 443 your-server.com.
Enabling additional logging for debugging¶
When you are chasing a problem inside the container, these environment variables give you more to work with:
TESTING_LOGGING=trueturns on debug logging for every e4a service in the container.LIBSEGFAULT_ENABLED=trueuses libsegfault, which reports more when a service crashes.
Set them in your override file and recreate the container with docker compose up -d:
services:
exchange4all:
environment:
- LIBSEGFAULT_ENABLED=true
- TESTING_LOGGING=true
Both produce a lot of output. Turn them off again once you have what you need.
Configuring the container/host for core dumps¶
Getting a core dump out of a crash takes changes on both the host and the container.
The kernel core pattern is a host-wide setting, so point it at a path that exists on the host and in the container alike. We recommend /storage/coredumps/. Put a quota on that filesystem — a crash loop writing dumps can otherwise fill the disk, and Exchange4all cannot recover from a full disk.
With ZFS:
mkdir -p /storage
zfs create -o quota=50G -o canmount=on -o mountpoint=/storage/coredumps \
rpool/USERDATA/coredumps
chmod 777 /storage/coredumps # 777 is used because various user ids in the container need write access to this location.
Set the pattern at runtime, which lasts until the next reboot:
echo '/storage/coredumps/core-%e.%h.%t.%p' | sudo tee /proc/sys/kernel/core_pattern
Or make it permanent:
cat <<EOF >>/etc/sysctl.d/99-coredump.conf
kernel.core_pattern=/storage/coredumps/core-%e.%h.%t.%p
EOF
sysctl -p /etc/sysctl.d/99-coredump.conf
To configure the container for core dumps, download the ready made override file next to your docker-compose.yml:
curl -fsSL https://manual.myexchange.rocks/on-premise/coredumps.override.yml -o docker-compose.override.yml
docker compose up -d
Its checksum is listed in SHA256SUMS. If you already have an override file, merge these contents into it rather than overwriting it:
# Example docker-compose.override.yml that enables core dumps.
#
# Copy this file next to your docker-compose.yml, rename it to docker-compose.override.yml
# and recreate the container with: docker compose up -d
#
# The host needs to be prepared as well, otherwise no dump is ever written:
#
# mkdir -p /storage/coredumps
# chmod 777 /storage/coredumps
# echo 'kernel.core_pattern=/storage/coredumps/core-%e.%h.%t.%p' >/etc/sysctl.d/99-coredump.conf
# sysctl -p /etc/sysctl.d/99-coredump.conf
#
# The core pattern is a kernel setting shared by the host and every container on it, so the path
# has to exist on both sides and is mounted below. Put a quota on it, a crash loop can otherwise
# fill the disk and Exchange4all cannot recover from a full disk.
#
# See https://manual.myexchange.rocks/on-premise/#configuring_the_containerhost_for_core_dumps
services:
exchange4all:
volumes:
# left side is the host, right side the container, both must match the core pattern above
- /storage/coredumps/:/storage/coredumps/
ulimits:
# no limit on the size of a written core file
core: -1
environment:
# better stack traces in the log when a service crashes
- LIBSEGFAULT_ENABLED=true
Testing if core dumps are generated¶
Force a core dump of the http process to confirm your configuration works:
This interrupts user connections
Do not run it on a production deployment.
kill -SEGV $(pidof http)
Reporting crashes to the maintainers¶
Before you report a crash, make sure core dumps are configured and one has actually been written.
Include all of the following:
-
The backtrace from the log. You can also extract it from the core file, as long as the version that produced it still matches what you are running:
gdb --cd=/opt/exchange4all/system/var/run /opt/exchange4all/system/libexec/imap /storage/core/filenameThen type
bt. Ask us for the matching debug symbols for your container if you need them. -
The dump file, compressed. Compress it before sending, uncompressed dumps are large:
gzip /storage/coredumps/core-http.exchange4all.local.1641398017.5206 -
The exact versions, both of the container and of the components inside it. Take the container version from your
.env, and the component versions from/opt/exchange4all/.version:docker compose exec exchange4all cat /opt/exchange4all/.version
Send everything to support@nhe4a.tech.
Getting (debug) logs from the ActiveSync component¶
The ActiveSync component is called push and writes these log files:
| File | Contains |
|---|---|
/storage/push/logs/running.log |
The default log: ActiveSync commands, memory usage, timings, device and user information. |
/storage/push/logs/user/userlog.log |
A truncated log of user activity — mail sent, and items changed, moved or created in a folder. |
/storage/push/logs/debug.txt |
Created automatically for matching errors. Define DEBUG_BACKTRACE_IGNORE_ARGS for more detail. |
To log one specific user, set LOG_USER_FILTER in /opt/exchange4all/push/config/config.php:
// debug log switch and filter
// user account should be split by ','
// empty string to log all users
// comment to totally disable log
define("LOG_USER_FILTER", "markus@example.com");
As soon as that user's client makes a request, you get a folder named after them under /storage/push/logs/dump/, holding one log file per connected device.
This configuration file does not survive a restart
/opt/exchange4all/push/config/config.php is part of the container filesystem, so your changes are overwritten whenever the container restarts or is recreated. Use it for a debugging session, not for permanent configuration.
Reverse Proxy configurations¶
When you cannot point the autodiscover domain straight at the container — because of DNS policy, infrastructure constraints or a customer's preference — redirect the traffic from an existing server instead. Requests to https://example.com/Autodiscover/Autodiscover.xml then end up at your deployment. This is a supported fallback, not the recommended deployment.
Add the following to the nginx.conf of that server:
location = /mail/config-v1.1.xml {
return 302 https://autodiscover.example.com$request_uri;
}
location /autodiscover/ {
return 302 https://autodiscover.example.com$request_uri;
}
location /Autodiscover/ {
return 302 https://autodiscover.example.com$request_uri;
}
autodiscover.example.com with your own autodiscover domain. Anything hitting those locations is then redirected to the autodiscover endpoint of your Exchange4all deployment.
Optional Environment Variables¶
Set these at container level, or where applicable for a single service. The generated docker-compose.yml passes some of them through from .env — for those, add the variable there and recreate the container with docker compose up -d. For all others, use an override file.
| Environment variable | Example | Description |
|---|---|---|
ACMESERVER |
staging |
ACME endpoint used for certificate retrieval |
DOMAINS |
mail.example.com example.com |
Domain names through which the system should be reachable. Used for certificate retrieval (if configured) and web server configuration. |
E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS |
true |
Enables experimental EWS operations. Defaults to false; only the exact value true enables them. |
E4A_LOG_LEVEL |
4 |
Controls the logging level of the e4a services. |
E4A_LOG_PUT_TIME |
0 |
Prefix for the time and date the log was created. |
E4A_SERVICED_MAILGEN_COMPANY_NAME |
ExampleCompany |
Company name to be used for generated mails. |
E4A_SERVICED_MAILGEN_LOGO_PATH |
/opt/exchange4all/manage/manage-webapp/ |
Logo to be used for generated mails |
E4A_SERVICED_MAILGEN_MAIL_FROM_ADDR |
no-reply@example.com |
Sending information to be used for generated mails |
E4AFQDN |
mail.example.com |
The main domain of the system. |
E4AMAILDOMAIN |
example.com |
First mail domain to be created (only relevant at first start) |
encryption_secret_key |
/storage/lico/lico-encryption-secret.key |
The location of the encryption secret used for auth tokens. |
identifier_registration_conf |
/storage/lico/identifier-registration.yaml |
The location of the OpenID provider registration. |
identifier_scopes_conf |
/storage/lico/scopes.yaml |
Location of the scopes configuration for the OpenID provider. |
LEGO_PATH |
/storage/.lego-tmp |
Location of the Let's Encrypt client data. |
LIBSEGFAULT_ENABLED |
true |
Use libsegfault for better stack traces on crashes (default) |
MARIADB_UNIX_PORT |
/run/mysqld/mysqld.sock |
MariaDB unix socket location |
NOPERMISSIONS |
true |
Don't chown /storage/system and /storage/service (only done on first start) |
signing_private_key |
/storage/lico/signing-private-key.pem |
location of private key used to sign auth tokens |
SUBWORKERS |
1 |
Number of worker processes for the e4a-manage-api |
ULIMIT_FRONTEND |
65536 |
Open file limit for front facing services |
ULIMIT_GENERAL |
8196 |
Open file limit for general services |
ULIMIT_IGNORE |
false |
Ignore container warning about low open file limit |
ULIMIT_NETWORKED |
102400 |
Open file limit for networked services |
validation_keys_path |
/storage/lico/validationkeys |
Location of the keys used to verify auth tokens |
Explanation of privilegeBits¶
When using the Manage API to create or modify users and domains, the following privilege bits can be specified. These are summed and then passed as a single value for privilegeBits.
User Privilege Bits¶
| Value | Name | Description |
|---|---|---|
0x1 |
POP3_IMAP |
if user can retrieve mail via pop3/imap |
0x2 |
SMTP |
if user can send email via SMTP |
0x4 |
CHGPASSWD |
if user can change password |
0x8 |
NO_MAPI |
if user can not use Outlook via MAPI over HTTP |
0x10 |
NO_AS |
if user can not use ActiveSync |
0x20 |
NO_WEB |
reserved, has no effect yet |
0x40 |
USRADM |
user administrator |
0x100 |
DOMADM |
domain administrator |
0x400 |
ORGADM |
Organisation Administrator |
0x1000 |
SYSADM |
System Administrator |
The three NO_ bits work the other way round from the first three: setting them takes access away. A user created with the default value 0x7 (POP3_IMAP, SMTP and CHGPASSWD) can therefore use every protocol. To keep a user on webmail and IMAP only, for example, add NO_MAPI and NO_AS: 0x7 + 0x8 + 0x10 = 0x1F, which is 31 in the API.
NO_MAPImakes MAPI over HTTP answer403 Forbidden, so Outlook for Windows cannot connect. EWS is not affected, so Outlook for Mac and other EWS clients keep working.NO_ASrejects ActiveSync logins, which blocks mobile devices and Windows Mail.NO_WEBis accepted by the API, but nothing checks it yet. Setting it does not keep a user out of the webmail.
The administration interface sets NO_MAPI and NO_AS through the options Can use Outlook and Can use mobile device sync (EAS) of a user.
Domain Privilege Bits¶
| Value | Name | Description |
|---|---|---|
0x1 |
ARCHIVE |
reserved for historical reasons via the archive_agent plugin, handled by daemon/slugs/sync_from_mysql_1.php |
0x2 |
MONITOR |
unused, reserved for historical reasons |
0x4 |
VERYFYD |
indicates that the domain has passed validation |
0x8 |
SUBSYSTEM |
Reserved for historical reasons via domain_subsystem plugin processed by daemon/slugs/sync_from_mysql_1.php |
0x10 |
NETDISK |
unused, reserved for historical reasons |
0x20 |
EXTPASSWD |
unused, reserved for historical reasons |
0x400 |
SUBROOT |
if domain is available as parent for subdomain creation |
Auto Event Configuration Example¶
New feature in 8.0.5
This is a new feature which is not fully implemented yet. Therefore some of the modifiers do not work as expected. In the final implementation the configuration file will not be modified by admins directly, but instead these options will be exposed through the manage api.
# ============================================================================
# Exchange4all Auto Event Configuration Example
# ============================================================================
# This file configures automatic processing of meeting requests.
#
# Backward Compatibility:
# - File missing: Adds meetings to calendar without auto-response
# - File empty: Checks free/busy status before accepting
# - File with content: Uses configuration below
#
# Location: {mailbox_directory}/config/auto_event.cfg
# ============================================================================
# ----------------------------------------------------------------------------
# Default Action (Required)
# ----------------------------------------------------------------------------
# Specifies the primary action to take for incoming meeting requests
#
# Available options:
# "add_to_calendar" - Add meeting to calendar without sending response
# "decide_based_on_freebusy" - Check availability and accept/reject accordingly
# "accept_and_add" - Always accept and add to calendar
# "reject" - Always reject meeting requests
# "no_action" - Do not process meeting requests
#
# Default: "decide_based_on_freebusy" (same as empty config file)
DEFAULT_ACTION = "decide_based_on_freebusy"
# ----------------------------------------------------------------------------
# Meeting Response Action (Optional)
# ----------------------------------------------------------------------------
# Specifies the action to take for meeting response messages (attendee replies)
#
# Available options:
# "process_reply" - Process meeting responses to update calendar (default)
# "no_action" - Ignore meeting response messages
#
# Default: "process_reply" (same as current behavior)
RESPONSE_ACTION = "process_reply"
# ----------------------------------------------------------------------------
# Meeting Cancellation Action (Optional)
# ----------------------------------------------------------------------------
# Specifies the action to take when a meeting cancellation is received.
# Outlook/M365 sends cancellations as IPM.Schedule.Meeting.Canceled messages.
#
# Available options:
# "no_action" - Ignore cancellations, leave calendar entry in place (default)
# "delete_event" - Automatically delete the calendar entry when a cancellation arrives
#
# For resource accounts (conference rooms, equipment) set to "delete_event" so the
# slot becomes available for re-booking without manual intervention.
#
# Default: "no_action" (backward-compatible)
#CANCELLATION_ACTION = "delete_event"
# ----------------------------------------------------------------------------
# Action Modifiers (Optional)
# ----------------------------------------------------------------------------
# Override the default action for specific meeting types or conditions
# If not specified, the DEFAULT_ACTION is used for all meetings
[MODIFIERS]
# Action for recurring meeting requests
# Set to "reject" to decline all recurring meetings (Kopano --mr-decline-recurring)
# Available: "accept_and_add", "reject", "decide_based_on_freebusy", "add_to_calendar", "no_action"
#RECURRING_MEETINGS = "reject"
# Action for meetings that conflict with existing calendar entries
# Set to "reject" to decline conflicting meetings (Kopano --mr-decline-conflict)
# Only used if the selected action is "decide_based_on_freebusy"
# Available: "accept_and_add", "reject" (default), "add_to_calendar", "no_action"
#CONFLICTING_MEETINGS = "reject"
# Action for meetings scheduled too far in advance
# Helps prevent booking resources too far into the future
# Available: "accept_and_add", "reject", "decide_based_on_freebusy", "add_to_calendar", "no_action"
#TOO_FAR_ADVANCE = "reject"
# Action for too long meetings
# Available: "accept_and_add", "reject", "decide_based_on_freebusy", "add_to_calendar", "no_action"
#TOO_LONG = "reject"
# Action for meetings scheduled outside configured working hours
# Useful for resource booking systems with restricted availability
# Available: "accept_and_add", "reject", "decide_based_on_freebusy", "add_to_calendar", "no_action"
#OUTSIDE_WORKING_HOURS = "reject"
# ----------------------------------------------------------------------------
# Modifier Conditions (Optional)
# ----------------------------------------------------------------------------
# Configure the conditions that trigger the modifiers above
[CONDITIONS]
# Maximum number of days in advance to accept meeting requests
# Meetings beyond this limit will trigger TOO_FAR_ADVANCE modifier
# Range: 1-36500 (100 years), Default: 365
MAX_DAYS_ADVANCE = 365
# Maximum duration of meetings in minutes to accept meeting request
# Meetings longer than this limit will trigger TOO_LONG modifier
# Range: 1-525600 (1 year), Default: 1440
MAX_DURATION = 1440
# Working hours configuration (24-hour format)
# Used by OUTSIDE_WORKING_HOURS modifier
# Format: "HH:MM", Default: "09:00" and "17:00"
WORKING_HOURS_START = "09:00"
WORKING_HOURS_END = "17:00"
# Working days of the week
# Used by OUTSIDE_WORKING_HOURS modifier for weekend/holiday handling
# Available: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
# Default: Monday through Friday
WORKING_DAYS = ["mon", "tue", "wed", "thu", "fri"]
# Overbooking capacity
# New meetings will not be considered a conflict if the number of conflicting
# meetings is not above the configured capacity.
# Used by CONFLICTING_MEETINGS modifier
# Range: 1-1000, Default: 1
OVERBOOKING_CAPACITY = 1
Configuring aliases in Postfix¶
In Exchange4all e-mail aliases and groups can easily be created through the Manage console and RestAPI. In addition to this it is also possible to configure aliases directly in the containers Postfix instance.
The aliases file is stored at /storage/postfix-virtual-alias and will automatically be created (along with an alias for the root mailbox) if it does not yet exist.
# remember to run postmap /storage/postfix-virtual-alias after making changes
root@exchange4all.local admin@exchange4all.local
user1@exchange4all.local user1@exchange4all.local user2@exchange4all.local
user3@exchange4all.local external@example.com
To apply changes run postmap /storage/postfix-virtual-alias after editing the file.
Inspecting and Managing the Postfix Message Queue¶
If the delivery service (e4a-delivery) is crashing, the cause is often a problematic message stuck in the local Postfix queue.
List all queued messages:
mailq
Inspect a specific message:
postqueue -p <queue-id>
Remove a message from the queue:
postsuper -d <queue-id>
Removing the offending message is usually sufficient to stop the crash. Once resolved, restart the delivery service with:
rm /etc/runit/runsvdir/default/e4a-delivery/down && sv up e4a-delivery
Send us the message before you delete it
Share its content with support first, so we can fix what made it crash the service.