From 2bb6ed310e82269a741cee654531734efa0c04e9 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Tue, 1 Sep 2026 16:02:52 +0100 Subject: [PATCH] =?UTF-8?q?feat(dovecot):=20shared=20mailboxes=20=E2=80=94?= =?UTF-8?q?=20info@=20reads=20all=20accounts=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dovecot ACL + shared namespace (shared/%%u/), acl_shared_dict in mail-state, idempotent user-patches.sh grants info@ read-only (lookup read) on every mailbox of ano@, andriy.oblivantsev@, postmaster@ and pre-subscribes the shared folders so Roundcube's subscribed folder list shows them. Delivery and passwords untouched; other accounts see no shared folders. --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- config/dovecot.cf | 40 ++++++++++++++++++++++++++++++++++++++++ config/user-patches.sh | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 config/dovecot.cf create mode 100755 config/user-patches.sh diff --git a/README.md b/README.md index 8ff7f1e..cfc9e01 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,12 @@ image) and is reachable at **https://mail.produktor.io** (alias `172.17.0.1:19944`, Let's Encrypt). Login: any mailbox address from the table above + its real password. The UI -shows one mailbox per login; to see all accounts, log in with each one. The -account list is the `postfix-accounts.cf` file (see Accounts). +shows one mailbox per login; the account list is the `postfix-accounts.cf` +file (see Accounts). + +Since the shared-mailbox setup (below) `info@` additionally sees every other +mailbox under `Shared/` and can read them — one login covers the whole +account list. Connection details used by the webmail (IMAP/SMTP): @@ -57,6 +61,40 @@ The webmail stores its sqlite database (addressbook, settings) in `data/roundcube/db/`. `ROUNDCUBEMAIL_DES_KEY` (session encryption) must be set in `.env` — compose fails without it. +## Shared mailboxes (единый вход info@) + +`info@produktor.io` can read all mailboxes (`ano@`, `andriy.oblivantsev@`, +`postmaster@`) as read-only shared folders — one login in Roundcube covers the +whole account list. Delivery is unchanged (no aliases, no redirects); other +accounts keep their own passwords and full rights. + +How it works (Dovecot 2.3 ACL + shared namespace): + +- `config/dovecot.cf` (→ `/etc/dovecot/local.conf`) enables the `acl` plugin, + adds a shared namespace `shared/%%u/` (`list=children`, read index per + reader via `INDEXPVT`), and points `acl_shared_dict` to + `/var/lib/dovecot/db/shared-mailboxes.db` (persistent via `mail-state`). +- `config/user-patches.sh` re-applies read-only (`lr`) ACLs from each shared + owner's mailboxes to `user=info@produktor.io` via `doveadm acl set` — the + only way Dovecot records the share in the shared dictionary — and + pre-subscribes the shared folders for `info@`. DMS runs it on the first + start of each container instance (plain `docker compose restart` skips the + setup step by design); ACLs, the shared dict and subscriptions persist in + `mail-state`/maildirs, so nothing is lost on restarts. Idempotent — safe to + run manually: `docker exec mailserver /bin/bash /tmp/docker-mailserver/user-patches.sh`. + +Upgrade behavior (image `:latest`): the config survives container recreation +because both files live in the mounted `config/`. On image upgrade the +entrypoint re-applies `dovecot.cf` and runs `user-patches.sh` again on the new +container's first start, so ACLs and subscriptions are recreated. The only +state kept outside the repo is `shared-mailboxes.db` (inside +`data/mail-state/`); if it is lost, the next (re)creation rebuilds it via +`doveadm acl set`. + +Limitation (Dovecot semantics): new mailboxes created by an owner *after* the +last start do not inherit the share (no ACL inheritance); they appear for +`info@` after the next container start. + ## Reverse proxy (NPM) `mail.produktor.io` is a proxy host in Nginx Proxy Manager (`provider` container, diff --git a/config/dovecot.cf b/config/dovecot.cf new file mode 100644 index 0000000..087cb44 --- /dev/null +++ b/config/dovecot.cf @@ -0,0 +1,40 @@ +# Dovecot hardening (E2 D3). +auth_failure_delay = 2s +mail_max_userip_connections = 10 + +# --- Dovecot shared mailboxes (issue #79): info@ reads all mailboxes. --- +# Canonical Dovecot 2.3 shared-mailbox scheme: acl plugin + shared namespace + +# acl_shared_dict. The imap plugin list is explicit: inside a `protocol imap {}` +# filter $mail_plugins expands to the filter-level value from DMS's 20-imap.conf +# (which shadows the global), so a `$mail_plugins acl` line would silently drop +# `acl`. Keep DMS's imap_quota to not regress quota IMAP commands. +mail_plugins = " quota acl" +protocol imap { + mail_plugins = " quota acl imap_quota" +} + +# Dovecot merges namespace blocks with the same identity (type+prefix); this +# makes the default (maildir) "." separator explicit "/" so it matches the +# shared namespace below ("All list=yes namespaces must use the same separator"). +namespace inbox { + separator = / +} + +namespace { + type = shared + separator = / + prefix = shared/%%u/ + location = maildir:/var/mail/%%d/%%n:INDEXPVT=~/shared/%%u + # subscriptions=yes: Roundcube's folder list is subscribed-based (LIST-EXTENDED + # SUBSCRIBED / LSUB); without per-user subscriptions shared folders would be + # invisible in the web UI. user-patches.sh pre-subscribes them for info@. + subscriptions = yes + list = children +} + +plugin { + acl = vfile + # Required for the shared namespace LIST to work: tracks "who shared to whom". + # /var/lib/dovecot is a symlink to /var/mail-state/lib-dovecot (persistent). + acl_shared_dict = file:/var/lib/dovecot/db/shared-mailboxes.db +} diff --git a/config/user-patches.sh b/config/user-patches.sh new file mode 100755 index 0000000..6203619 --- /dev/null +++ b/config/user-patches.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Dovecot shared mailboxes (issue #79): info@produktor.io gets read-only (lr) +# access to the mailboxes of ano@, andriy.oblivantsev@, postmaster@produktor.io. +# DMS runs this only on the FIRST start of each container instance (plain +# `docker compose restart` skips the setup step by design — /CONTAINER_START +# marker), so it must stay idempotent. ACLs, the shared dict and subscriptions +# persist in mail-state / maildirs across restarts. +set -euo pipefail + +# 1. acl_shared_dict directory: must exist and be writable by the mail user. +SHARED_DB_DIR=/var/lib/dovecot/db +mkdir -p "${SHARED_DB_DIR}" +chown docker:docker "${SHARED_DB_DIR}" +chmod 0770 "${SHARED_DB_DIR}" + +# 2. Grant info@ read-only rights on every current mailbox of the shared owners. +# doveadm acl set is the only way Dovecot records the share in acl_shared_dict +# (manual dovecot-acl files do NOT populate the dictionary — Dovecot docs). +# NOTE: this Dovecot build accepts full right NAMES ("lookup read"), single +# letters ("lr") are rejected with "Invalid right". +READER='info@produktor.io' +for owner in ano@produktor.io andriy.oblivantsev@produktor.io postmaster@produktor.io; do + # The shared mailbox "shared/" maps to the owner's INBOX (Dovecot + # shared-storage semantics) — subscribe it explicitly so Roundcube's + # subscribed folder list shows it. + doveadm mailbox subscribe -u "${READER}" "shared/${owner}" + for mb in $(doveadm mailbox list -u "${owner}"); do + doveadm acl set -u "${owner}" "${mb}" "user=${READER}" lookup read + if [ "${mb}" != "INBOX" ]; then + doveadm mailbox subscribe -u "${READER}" "shared/${owner}/${mb}" + fi + done +done