Repository navigation
feat(mail): make mailcow a selectable mail backend, installable from the panel - #12
Open
redstonerthebest wants to merge 12 commits into
Open
redstonerthebest wants to merge 12 commits into
redstonerthebest wants to merge 12 commits into
Conversation
system.mail.backend now selects the stack the panel manages: docker-mailserver (default, behaviour unchanged) or mailcow: dockerized. - IMailBackend + MailBackendFactory: MailManager keeps payload parsing, event hooks and bookkeeping (domains file, autoresponder state, lists) and delegates the stack specific work, so /api/mail/* stays identical for both backends. - DockerMailserverBackend: the previous MailManager logic, unchanged. - MailcowBackend: domains, mailboxes, passwords, enable/disable, aliases, DKIM and the per-mailbox spam score via mailcow's REST API; autoresponder via doveadm sieve inside the dovecot container (no mailbox password required). delete/alias resolves the alias id first, because mailcow deletes by id. - HostPackageManager: new "mailcow" package next to "mailserver" (compose fetch, mailcow.conf generation for hostname/ports/ACME, up/down on purge). - MailProbe, StartupSelfTest and MailDnsHelper are backend aware; mailcow DKIM keys are read from data/dkim/<domain>/<selector>.txt. - Unknown backend values are rejected instead of silently managing the other stack. - README "Mail backends" section plus tests for backend selection, mailcow payloads, alias id resolution and the autoresponder script.
…ailcow Upstream removed mailcow.conf.example and moved DKIM keys into redis; the mailcow backend of this branch was written against the older layout. - mailcow.conf is now created by mailcow's own generate_config.sh, run non-interactively (hostname/timezone/ClamAV answer passed in, --dev keeps it from checking out a branch, stdin feeds the overwrite prompt) and afterwards only the keys the panel owns are overridden. A checkout without the script gets a documented fallback file instead of an empty config. - API_KEY/API_KEY_READ_ONLY/API_ALLOW_FROM are generated into mailcow.conf and the same key is written to feather-api-key, so nothing has to be pasted into the mailcow UI; the UI/API is published on loopback only (the panel's proxy owns 80/443) and SKIP_IP_CHECK is set because docker rewrites the source IP. - ClamAV is disabled below 2.5 GiB RAM, mirroring the installer's own recommendation, so the unattended install cannot hang on that prompt. - DKIM hints read the public record through GET /api/v1/get/dkim/<domain> (redis-backed, chunks joined); the data/dkim/<domain>/<selector>.txt candidates stay as a legacy fallback, and a non-object API answer (mailcow returns [] for a domain without a key) no longer throws. - docs/mail-backends.md documents the backend, the conf rules and the DKIM path; tests cover the conf rules, the key material and both DKIM sources.
The mailcow backend assumed the stack runs on the same node and gated every
operation on local docker containers, so a mailcow on a dedicated mail host
(mail.mailcow.url + API key) was reported as "not running".
- IMailcowApi.PingAsync (default false) + MailcowApiClient implementation of
GET /api/v1/get/status/version: the API answering is what "running" means for
a remote stack.
- MailcowDocker.StackReachable() = local containers OR remote API answering;
RemoteHost() labels the target ("mail.allo.bet (remote)") for diagnostics.
- MailProbe.StackRunning/StackIdentifier and MailcowBackend.IsRunning use it, so
/api/mail/* works against a remote mailcow; MailcowBackend.ProbeStatus reports
mode (local|remote), the remote host and only requires local ports for local
stacks.
- MailController: probe and the best-effort DKIM generation use the backend-aware
probe instead of looking for the docker-mailserver container.
- tests: PingAsync (version document, missing key, error body) -> 456 tests.
Live test against a mailcow that runs on its own host found two payload bugs:
- add/domain sent "mailboxes":"0". mailcow's mailbox add rejects while
count >= domain.mailboxes (functions.mailbox.inc.php), so 0 means "no mailboxes
at all" and every mailbox on an auto-created domain failed with
max_mailbox_exceeded; 0; 0. Now driven by mail.mailcow.domain_mailbox_limit (default 10).
- delete/{domain,mailbox,alias} sent {"items":[..]}. For action=delete json_api.php sets
$_POST['items'] = <whole request body>, and the handler json_decodes it again, so the
wrapped object arrived as items = {"items":[..]} and mailcow answered access_denied.
The delete endpoints take the bare array body.
Verified live: auto-added domain, mailbox creation, IMAP/SMTP login, delete cycle.
Tests 460 pass.
MailDnsHelper.ResolveMailHostname() fell back to "mail.<domain>" whenever mail.hostname was empty, so a node with a remote mailcow published MX/SPF records pointing at a host that does not exist (seen live: MX mail.lennyplugins.dev for a mailcow reachable at mail.allo.bet). Prefer the configured hostname, then the mailcow host/url, and only invent mail.<domain> when nothing is configured. Tests: 463 pass (3 new).
A mail domain that only has MX/SPF/DKIM/DMARC works, but mail clients cannot configure themselves. The hints now also carry autodiscover/autoconfig (CNAME to the mail host) and the SRV set for IMAP/submission/POP3, marked `optional` so a panel or provider that cannot write them never fails the run. Ports come from the mail config (imap_port/smtp_port), POP3s uses the standard 995. Tests: 465 pass (2 new).
The deliverability checks probed 127.0.0.1 for the mail ports and measured the reverse DNS of the web node's IP, so a domain whose mail runs on a remote mailcow reported "not listening" and a failing PTR while the mail server was healthy. Ports are now probed on the configured mail host (loopback still accepted for a local stack) and the PTR check resolves the MX target's address instead of the node's; the node's own reverse DNS is reported separately because it only matters when that node sends mail itself.
Operators who wanted mailcow had to read the code: the doc described what the backend does, not how to get a stack running, the config keys were only visible in the source, and a mailcow on another host failed every /api/mail call with "not running on this node" although its API answered. - docs/mail-backends.md is now a setup guide: requirements, both paths (FeatherQuilld installs the stack / use a mailcow you already run), the full system.mail.* reference with defaults, DNS records, verification commands and a troubleshooting table. - NotRunningHint() as a default member on IMailBackend: the guard message now names the config keys to set or the host package to install. MailManager appends it, MailBackendFactory lists the valid values and points at the doc. - MailController gates on MailProbe.StackRunning() instead of the docker-mailserver container name, so the mail API also serves a remote mailcow. - Startup self-test: a configured remote mailcow reports api_key/API_ALLOW_FROM and ports instead of "install the package".
BuildHints_IncludesMxAndSpfWithoutDkimFile asserted that DKIM is not ready for example.com, but the config used the default root directory, so the assertion read /var/lib/featherquilld/mail on the machine running the tests. On a node that has ever provisioned example.com (or kept a key from a live test) the test fails while nothing is wrong with the code. Point the config at a throwaway root, like the other mail tests do.
The panel's package manager renders whatever GET /api/system/packages returns, so an operator installing mailcow from the panel saw a name and an Install button but no word about the backend switch or where the guide lives. - HostPackageStatus gains description and docs_url; the mail packages (and docker, which they depend on) fill them. The mailcow text names the config key to set (system.mail.backend: mailcow) and points at docs/mail-backends.md. - Tests assert both, so the panel keeps having something to show.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…pushed yet, test/code unchanged vs upstream)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FeatherQuilld: selectable mail backend (mailcow: dockerized)
What this adds
system.mail.backendselects the stack a node manages:docker-mailserver(default, aliasesdocker,docker-mail-server,dms)mailcow(aliasesmailcow-dockerized,mailcowdockerized)/api/mail/*stays identical for both backends (domains, mailboxes, passwords, enable/disable,aliases, spam filter, autoresponders, lists), so the panel and the WebSpace DNS provisioning do
not need to know which stack answers. An unknown value is rejected instead of silently managing
the other stack.
mailcow can either be installed by FeatherQuilld on the node itself (host package
mailcow,like
mailserver/webmailtoday) or already run on another host (system.mail.mailcow.url+api_key).Why
Nodes that already run mailcow had to be managed outside the panel, and the docker-mailserver
stack is not the right answer for every operator. The switch keeps existing installations
byte-identical (same default, same data paths, same API payloads) while making mailcow a
first-class option.
What the last commit improves (self-service)
Reviewing the branch from an operator's point of view showed three things that forced people to
read the code:
docs/mail-backends.mddescribed what the backend does, not how to get a mailcow running(requirements, config, DNS, verification, failure modes). It is now a setup guide for both
paths, with the complete
system.mail.*reference incl. defaults.… mail server is not running.NowIMailBackend.NotRunningHint()(a default interface member, so third-party backends and testfakes keep compiling) supplies the actionable text: which config keys to set, or which host
package to install.
MailManagerappends it,MailBackendFactorylists the valid values andpoints at the doc, and the startup self-test tells a local from a remote mailcow.
MailController.RequireManagergated onMailProbe.ContainerRunning(), i.e. on thedocker-mailserver container name. With a mailcow on another host every
/api/mail/*callfailed with "not running on this node" while the API answered. It now uses
MailProbe.StackRunning(), the predicate that already knows about remote stacks.Verification
git amof the eight patches on top ofmaster(9d8c72a) in a fresh clone: clean.dotnet build: succeeded, 0 errors.dotnet test --filter "FullyQualifiedName~Mail": 96/96 passed (new tests cover the factorymessage, both
NotRunningHint()variants - local and remote mailcow - and the guard message).Version 2026-09a) in the earliercommits of this branch:
add/*flat,edit|deletewith{items, attr}(the plain array fordelete/*),add/domainneeding a real mailbox limit (0 rejects every mailbox), DKIM readthrough
get/dkim/<domain>because current mailcow keeps keys in redis, and the API key comingfrom
mailcow.conf(API_KEY,API_ALLOW_FROM,SKIP_IP_CHECK=ywhen the API is onlypublished on loopback).
Notes for the reviewer
docker-mailserver, noconfig key is required, and nothing under
/var/libmoves.generate_config.shnon-interactively (upstreamremoved
mailcow.conf.example;.envis a symlink tomailcow.conf) and only then overridesthe keys the panel owns.
publishes the mailcow UI/API on loopback (
8080/8443). Operators can change both.doveadm sieveinside thedovecot container); everything else works against a remote stack.