A Docker Compose deployment kit for fetching mail from remote servers (POP3, IMAP), caching it locally, and serving it to internal users via IMAP and SMTP.
A small enterprise, team, or home user has one or more mailboxes hosted on public or paid services and needs to give access to one or more internal users, with some of the following goals:
- add or remove individual users without touching the real mailbox;
- avoid sharing the real mailbox password with end users;
- have a single point of control for all mail accounts;
- have a single point for filtering / sorting (manually, or by attaching an external IMAP client);
- consolidate mail storage in one place for regular backups;
- provide mail access when internet connectivity is restricted or isolated;
- cache messages locally but delete them from the remote server — immediately or after a configured retention period — to save space at the provider.
Configuration is file-based (no GUI). You define the external mailboxes (addresses, servers, credentials) and the local users who get access to them.
Each mailbox can have one or more local users.
user 1 --\
user 2 --+--- mailbox 1 (mailproxy) --- mailbox 1 (gmail.com)
user 3 --/
user 4 --\
user 5 --+--- mailbox 2 (mailproxy) --- mailbox 2 (yourcompany.com)
External mail clients (Outlook, Thunderbird, etc.) connect using the local usernames and passwords. A web client (Roundcube) can optionally be deployed.
The proxy regularly polls the real mail servers, stores messages in local Maildir copies, and serves them to clients via IMAP. It also accepts outbound mail and forwards it through the real SMTP servers.
| Service | Image | Role |
|---|---|---|
| fetcher | Python 3.11 | Polls POP3/IMAP, delivers to Maildir, generates credential maps for Dovecot and Postfix |
| dovecot | Debian / Dovecot 2.4 | IMAP server (ports 143 / 993) |
| postfix | Debian / Postfix 3.x | SMTP relay with per-sender authentication (port 587) |
| roundcube (optional) | roundcube/roundcubemail 1.6 | Web UI, enabled via ENABLE_ROUNDCUBE=true in .env |
# 0. Install prerequisites
apt install git docker.io docker-compose-plugin
# 1. Clone the repository (into /opt/mailproxy)
cd /opt
git clone https://github.com/stvorl/mailproxy.git
cd mailproxy
chmod +x mpcontrol
# 2. Choose: new deployment (2A) or restore from backup (2B)
# 2A. Create config files from templates
./mpcontrol init
# Fill in credentials and settings as indicated by the comments in each file.
nano accounts.yml
# In short: add mailbox entries with remote server details (host, login,
# password, retention period, poll interval). Add local users.
nano .env
# In short: enable Roundcube if needed; set which ports IMAP, SMTP, and
# Roundcube should listen on, and whether they should be network-accessible.
# 2B. Restoring from another machine — use ./mpcontrol import <backup file>:
# it restores logs, certs, maildata, rcdata, accounts.yml, .env
./mpcontrol import backup.tgz # your backup file
# 3. Start
./mpcontrol up
# 4. Verify
# 4.1 Watch logs:
./mpcontrol logs
# 4.2 Check that containers started and opened the expected ports:
docker compose ps
# 4.3 Open Roundcube in a browser and/or configure a mail client
# (Thunderbird, Outlook, etc.) to test local user access..env # service configuration
mpcontrol # management script
accounts.yml # credentials — NOT committed (.gitignore)
accounts.example.yml # annotated template
certs/ # TLS certificates for IMAP and SMTP
docker/
fetcher/ # mail fetcher service
dovecot/ # Dovecot service
postfix/ # Postfix service
roundcube/ # Roundcube service
plugins/
fix_identity/ # plugin: alias redirect + From address fix
logs/ # container logs on host — NOT committed
maildata/ # Maildirs — NOT committed
rcdata/ # Roundcube SQLite database — NOT committed
All proxied mail accounts are described in accounts.yml. The fetcher re-reads the file on every poll cycle — no container restart is needed.
global:
default_fetch_interval: 15 # minutes
accounts:
- address: alice@example.com
inbound: # fetch mail from remote POP3
host: pop.example.com
port: 995
proto: pop3
tls: true
user: alice@example.com
pass: remote_password
keep_remote: false # false — delete immediately; true — keep forever; 7d — 7 days
outbound: # SMTP relay for sending
host: smtp.example.com
port: 587
tls: true
user: alice@example.com
pass: smtp_password
local: # IMAP credentials (independent from remote)
mailbox: alice@example.com # local IMAP/SMTP login
password: local_password # local IMAP/SMTP passwordlocal.mailbox and local.password are required.
Several users can share a single mailbox — the same inbox, folders, and Roundcube address book — each with their own login and password, individually revocable.
# inbound and outbound sections as above
local:
mailbox: bob@example.org
password: default_pass
logins:
- user: bob
password: bobs_pass
- user: carol
password: carols_passlocal.mailbox and local.password are required here as well.
When logins: is present, signing in as bob or carol is automatically redirected to the canonical account bob@example.org — both see the same folders, messages, and contacts. The master password (local.password: default_pass) continues to grant direct access under the canonical address but should not be shared with end users. Individual users can be removed or new ones added at any time.
| Value | Behaviour |
|---|---|
false or 0 |
Delete from remote immediately after fetching (default) |
true |
Keep on remote forever |
10m |
Delete after 10 minutes |
12h |
Delete after 12 hours |
7d or 7D |
Delete after 7 days |
3M |
Delete after 3 months (~90 days) |
30s |
Delete after 30 seconds |
Messages are downloaded only once regardless of keep_remote. Fetch state is tracked in .fetch_state_*.json files in the mailbox directory.
When keep_remote: true and the remote mailbox holds thousands of old messages, every poll cycle asks the server to list them all. fetch_depth tells the fetcher to issue SEARCH SINCE <date> instead of SEARCH ALL, drastically reducing the UID list returned.
inbound:
proto: imap
keep_remote: true
fetch_depth: 30d # only consider messages from the last 30 daysAccepts the same suffixes as keep_remote: 30d, 3M, 12h, etc. Default: unlimited.
Interaction with keep_remote timedelta: if keep_remote is also a duration, the effective search depth is automatically extended to keep_remote + 10 × fetch_interval. This ensures messages near their expiry deadline remain visible to the fetcher long enough to be deleted. A log line is emitted when the adjustment occurs.
Interaction with keep_remote: false: only messages within fetch_depth are fetched, stored, and deleted from the remote server. Messages older than fetch_depth are neither fetched nor deleted.
fetch_depth has no effect for POP3 — the protocol always returns a full message list, which may slow down new mail retrieval on mailboxes with a large number of undeleted messages. In this case, prefer IMAP fetching and configure fetch_depth.
If the provider does not support POP3, or you need to fetch from a specific folder, use proto: imap:
inbound:
host: imap.example.com
port: 993
proto: imap
tls: true
folder: INBOX, News, Sent>Sent # optional; defaults to "INBOX" if omitted
user: charlie@example.com
pass: remote_password
keep_remote: 5d # delete after 5 daysfolder accepts a comma-separated list of mappings:
REMOTEmeansREMOTE>INBOX(fetched from remoteREMOTE, stored into localINBOX).REMOTE>LOCALmaps a remote folder to a specific local folder.
Example: INBOX, News, Sent>Sent is interpreted as INBOX>INBOX, News>INBOX, Sent>Sent.
Sent>Sent is useful when some users send mail directly via the remote provider:
those messages appear in remote Sent and can be mirrored into local Sent so they
are visible to all users sharing this proxied mailbox.
Another valid use case is the initial migration to this solution set, including importing the Sent folder history.
During each IMAP fetch cycle, mailproxy updates maildata/<mailbox>/imap_folders.txt.
This file contains one remote folder per line with two tab-separated columns:
the full folder path and the LIST flags returned by the remote IMAP server.
Some providers expose non-standard/localized system folder names. So, Sent folder could have some another name.
To configure Sent correctly, inspect imap_folders.txt and choose the folder
that has the \Sent flag.
.env controls how the services are exposed on the network.
An optional web mail client that sits on top of the IMAP/SMTP stack. When using it exclusively, external IMAP/SMTP access can be disabled to prevent third-party mail clients from connecting.
Set ENABLE_ROUNDCUBE=true in .env to include Roundcube in the stack.
Roundcube is available by default at http://<hostname>:8080.
ENABLE_ROUNDCUBE=true # uncomment to enable the web interface
ROUNDCUBE_LISTEN=0.0.0.0 # bind address (0.0.0.0 = all interfaces, 127.0.0.1 = localhost only)
ROUNDCUBE_PORT=8080 # HTTP port
ROUNDCUBE_TITLE=Roundcube # custom browser/tab title (optional)Two ports: 143 (plain/STARTTLS) and 993 (IMAPS). Bind address and TLS level are configured independently.
IMAP_TLS |
Behaviour |
|---|---|
1 |
Plain, no TLS offered |
2 |
STARTTLS available (on client request) |
3 |
STARTTLS required + port 993 (IMAPS) enabled |
Recommended combinations:
| Scenario | IMAP_LISTEN |
IMAPS_LISTEN |
IMAP_TLS |
|---|---|---|---|
| Local only (e.g. for Roundcube) | 127.0.0.1 |
127.0.0.1 |
1 |
| LAN, STARTTLS + IMAPS | 0.0.0.0 |
0.0.0.0 |
2 |
| LAN or internet, IMAPS only | 127.0.0.1 |
0.0.0.0 |
3 |
IMAP_LISTEN=127.0.0.1
IMAP_PORT=143
IMAPS_LISTEN=0.0.0.0
IMAPS_PORT=993
IMAP_TLS=3Optional full-text search acceleration (Dovecot FTS / xapian):
# true = enable FTS index/search acceleration, false = disable
# default: false
DOVECOT_FTS=falseNotes:
- The image already contains the required FTS plugin; switching
DOVECOT_FTSonly needs./mpcontrol restart(no rebuild). - When enabling FTS on an existing deployment, indexing is built over time as mailboxes are accessed.
Outbound mail proxy that forwards messages to the real SMTP servers.
Single port 587 (submission). Same parameters:
SMTP_TLS |
Behaviour |
|---|---|
1 |
Plain, no TLS |
2 |
STARTTLS available |
3 |
STARTTLS required |
SMTP_LISTEN=0.0.0.0
SMTP_PORT=587
SMTP_TLS=3All management goes through the mpcontrol script.
| Command | Description |
|---|---|
./mpcontrol init |
Create working configs from templates, prepare for first run |
./mpcontrol up |
Build images and start all services |
./mpcontrol reindex [mailbox ...] |
Rebuild Dovecot full text search indexes. No arguments: all account mailboxes from accounts.yml (accounts[].address). With arguments: only listed mailboxes, e.g. ./mpcontrol reindex mail1@somewhere.com mail2@somewhere.com. |
./mpcontrol down |
Stop all services |
./mpcontrol restart |
down + up |
./mpcontrol rebuild |
Force-rebuild images without Docker cache (use after base image updates) |
./mpcontrol logs |
Tail logs from all services |
./mpcontrol export <file> |
Stop services and pack maildata/, rcdata/, certs/, logs/, accounts.yml, .env into an archive |
./mpcontrol import <file> |
Unpack archive into project directory (does not start services) |
./mpcontrol clean |
Stop services and permanently delete all data (maildata/, rcdata/, certs/, accounts.yml, .env). Requires typing YES to confirm. |
When DOVECOT_FTS=true, reindex also refreshes full-text search indexes. With DOVECOT_FTS=false, it refreshes only base Dovecot mailbox indexes.
- All services listen on
127.0.0.1by default. SetIMAP_LISTEN,IMAPS_LISTEN,SMTP_LISTEN,ROUNDCUBE_LISTENin.envto expose them on the network. - When exposing services externally, set
IMAP_TLS=3andSMTP_TLS=3to enforce TLS. A self-signed certificate is generated automatically incerts/on first start. Replace it with a real certificate if needed. - For backups, use
./mpcontrol exportor copy the directoriesmaildata/,rcdata/,logs/,certs/and the files.env,accounts.yml— e.g. with borg, restic, etc. Everything else can be restored viagit clone.
To move mail archive, configured accounts, logs, certificates, and server settings to another machine:
./mpcontrol export backup.tgz
# copy the file to the target machineFollow steps 0–1, 2B, and 4 from the Installation section above.
# 1. Save a backup (just in case)
./mpcontrol export backup-before-upgrade.tgz
# 2. Stop services
./mpcontrol down
# 3. Pull updates from the repository
git pull
# 4. Rebuild images without cache
./mpcontrol rebuild
# 5. Start
./mpcontrol upAfter upgrading, check logs for errors: ./mpcontrol logs.
The directories maildata/, rcdata/, and logs/ contain the performance-critical data. If needed, they can be moved to dedicated physical or logical volumes, with symlinks left in place.