<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.wilsoz.com/index.php?action=history&amp;feed=atom&amp;title=Email%2FArchitecture</id>
	<title>Email/Architecture - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.wilsoz.com/index.php?action=history&amp;feed=atom&amp;title=Email%2FArchitecture"/>
	<link rel="alternate" type="text/html" href="https://wiki.wilsoz.com/index.php?title=Email/Architecture&amp;action=history"/>
	<updated>2026-07-06T07:00:27Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://wiki.wilsoz.com/index.php?title=Email/Architecture&amp;diff=24&amp;oldid=prev</id>
		<title>Wikibot: Migrated from TODO.md email section</title>
		<link rel="alternate" type="text/html" href="https://wiki.wilsoz.com/index.php?title=Email/Architecture&amp;diff=24&amp;oldid=prev"/>
		<updated>2026-07-05T18:18:17Z</updated>

		<summary type="html">&lt;p&gt;Migrated from TODO.md email section&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;&amp;#039;&amp;#039;As of: 2026-07-05 — migrated from `TODO.md` (Email section) and cross-referenced with [[Services/Stalwart]]&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
See also: &amp;#039;&amp;#039;&amp;#039;[[Services/Stalwart]]&amp;#039;&amp;#039;&amp;#039; for the full mail server operational reference (admin API, ports, sieve rules, gotchas). This page covers the broader mail architecture goals and phase history.&lt;br /&gt;
&lt;br /&gt;
== Email (Stalwart Migration) ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture goal: Multi-user family &amp;amp; friends mail server ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
Inbound:  Internet → MXRoute (per-user spool) → getmail → Stalwart (per-user inbox)&lt;br /&gt;
Outbound: User authenticates to Stalwart SMTP with own Authentik creds&lt;br /&gt;
          → Stalwart relays via user&amp;#039;s own MXRoute credentials (per-user relay routes)&lt;br /&gt;
          → 400 msg/hr global rate limit to MXRoute enforced by Stalwart queue throttle&lt;br /&gt;
Domains:  craniumslows.com now. wilsoz.com → MXRoute once craniumslows validated.&lt;br /&gt;
Users:    Authentik LDAP → Stalwart. Custom attributes drive address provisioning.&lt;br /&gt;
Spam:     Server-wide Stalwart sieve → spam into each user&amp;#039;s Junk&lt;br /&gt;
Sieve:    Per-user rules via ManageSieve (no rule affects another user)&lt;br /&gt;
Webmail:  SnappyMail at mymail.wilsoz.com&lt;br /&gt;
External: Gmail/iCloud/SDF as SnappyMail additional accounts (direct IMAP, not getmail)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== User provisioning model (Authentik custom LDAP attributes) ===&lt;br /&gt;
&lt;br /&gt;
Each Authentik user gets three custom attributes:&lt;br /&gt;
* &amp;lt;code&amp;gt;mail-name&amp;lt;/code&amp;gt; — primary local part (e.g. &amp;lt;code&amp;gt;joe&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &amp;lt;code&amp;gt;mail-alias&amp;lt;/code&amp;gt; — secondary local part, optional (e.g. &amp;lt;code&amp;gt;joe.smith&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &amp;lt;code&amp;gt;default-domain&amp;lt;/code&amp;gt; — which domain is the primary SnappyMail identity&lt;br /&gt;
&lt;br /&gt;
Groups control which domains are active:&lt;br /&gt;
* &amp;lt;code&amp;gt;craniumslows-email&amp;lt;/code&amp;gt; → @craniumslows.com addresses&lt;br /&gt;
* &amp;lt;code&amp;gt;wilsoz-email&amp;lt;/code&amp;gt; → @wilsoz.com addresses&lt;br /&gt;
&lt;br /&gt;
Address matrix = &amp;lt;code&amp;gt;{mail-name, mail-alias} × {domains from group memberships}&amp;lt;/code&amp;gt;&lt;br /&gt;
All addresses on a user deliver to one Stalwart inbox. Each address gets its own&lt;br /&gt;
MXRoute account + getmail .rc + per-user Stalwart relay route.&lt;br /&gt;
&lt;br /&gt;
Address changes: old address becomes a forwarder to the new one. Inbox never moves.&lt;br /&gt;
&lt;br /&gt;
=== Where the real mail data lives ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Location&lt;br /&gt;
! What&lt;br /&gt;
! Size&lt;br /&gt;
! Action&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/library/email/selfhosted/mail/wilsoz.com/marcus/Maildir/&amp;lt;/code&amp;gt;&lt;br /&gt;
| Primary mailbox — Mailcow/Dovecot, ~100 nested folders&lt;br /&gt;
| ~8.9GB&lt;br /&gt;
| &amp;#039;&amp;#039;&amp;#039;Import to Stalwart — in progress&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/library/email/selfhosted/mail/wilsoz.com/marcus/Orig-Maildir/&amp;lt;/code&amp;gt;&lt;br /&gt;
| Pre-Mailcow import (iRedMail/Gmail era)&lt;br /&gt;
| (subset)&lt;br /&gt;
| Verify not unique before discarding&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/library/email/selfhosted/mail/craniumslows.com/marcus/Maildir/&amp;lt;/code&amp;gt;&lt;br /&gt;
| craniumslows.com mailbox&lt;br /&gt;
| 152K&lt;br /&gt;
| Import to Stalwart&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/var/mail/mnw&amp;lt;/code&amp;gt;&lt;br /&gt;
| Old iRedMail mbox (pre-Mailcow era)&lt;br /&gt;
| 37MB&lt;br /&gt;
| Import after primary migration&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/mailcow-dockerized/&amp;lt;/code&amp;gt;&lt;br /&gt;
| Mailcow app stack only&lt;br /&gt;
| 49MB&lt;br /&gt;
| Remove after migration verified&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Phase 3 (Mail Import) — COMPLETE ✓ ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Import marcus@wilsoz.com&amp;#039;&amp;#039;&amp;#039; — 137,335 messages from Mailcow Maildir, all folders preserved&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Import marcus@craniumslows.com&amp;#039;&amp;#039;&amp;#039; — done (was tiny, only pre-Stalwart messages)&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Verify in SnappyMail&amp;#039;&amp;#039;&amp;#039; — folder hierarchy confirmed, messages accessible&lt;br /&gt;
* [ ] &amp;#039;&amp;#039;&amp;#039;Import old iRedMail mbox&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;/var/mail/mnw&amp;lt;/code&amp;gt; (37MB) not yet done (low priority, very old mail)&lt;br /&gt;
&lt;br /&gt;
=== Phase 4 (Getmail) — COMPLETE ✓ (one item remaining) ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Update delivery targets&amp;#039;&amp;#039;&amp;#039; in .rc files → Stalwart LMTP &amp;lt;code&amp;gt;localhost:24&amp;lt;/code&amp;gt;&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Create craniumslows-marcus.rc&amp;#039;&amp;#039;&amp;#039; — fetches marcus@craniumslows.com from MXRoute, 439/439 delivered&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Keep &amp;lt;code&amp;gt;delete = false&amp;lt;/code&amp;gt;&amp;#039;&amp;#039;&amp;#039; — correctly set; flip to true only after validating in daily use&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Create systemd timer&amp;#039;&amp;#039;&amp;#039; — getmail runs every 10 minutes via systemd&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Fastmail import COMPLETE&amp;#039;&amp;#039;&amp;#039; — 12,226/12,226 delivered (2026-02-22 ~15:52)&lt;br /&gt;
* [ ] ~~Configure SnappyMail additional accounts~~ — &amp;#039;&amp;#039;&amp;#039;ARCHITECTURE CHANGED&amp;#039;&amp;#039;&amp;#039; (see below): external accounts now use getmail, not SnappyMail connected accounts&lt;br /&gt;
&lt;br /&gt;
=== Architecture decision (2026-02-22) ===&lt;br /&gt;
Marcus&amp;#039;s external accounts (Gmail x2, iCloud, SDF) use &amp;#039;&amp;#039;&amp;#039;getmail&amp;#039;&amp;#039;&amp;#039; → Stalwart, NOT SnappyMail connected accounts. This gives a true unified inbox with sieve filtering. Phone/clients connect to one IMAP server.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Add getmail config: thewilson@gmail.com&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/gmail-thewilson.rc&amp;lt;/code&amp;gt; done. Import in progress: ~7,560/58,520 as of 2026-02-23 07:00 (~50 hrs remaining at current rate)&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Add getmail config: craniumslows@gmail.com&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/gmail-craniumslows.rc&amp;lt;/code&amp;gt; done. Will start after thewilson finishes.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Add getmail config: marcusthewilson@me.com&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/icloud-marcusthewilson.rc&amp;lt;/code&amp;gt; done.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Add getmail config: mnw@sdf.org&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/sdf-mnw.rc&amp;lt;/code&amp;gt; done. &amp;lt;code&amp;gt;delete=false&amp;lt;/code&amp;gt;.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Update systemd getmail.service&amp;#039;&amp;#039;&amp;#039; — all 6 rc files active, daemon reloaded 2026-02-23.&lt;br /&gt;
&lt;br /&gt;
=== Action needed — Phase 5 (Spam + Sieve) ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Fastmail import COMPLETE&amp;#039;&amp;#039;&amp;#039; — 12,226/12,226 (done 2026-02-22)&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Sieve rules written&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/marcus.sieve&amp;lt;/code&amp;gt; ready. Decade-based numbering, all rules merged. See &amp;lt;code&amp;gt;sieve/README.md&amp;lt;/code&amp;gt;.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Mail classification script written&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/classify-inbox.py&amp;lt;/code&amp;gt; done.&lt;br /&gt;
&amp;#039;&amp;#039; [x] &amp;#039;&amp;#039;&amp;#039;Audit actual Stalwart folder names&amp;#039;&amp;#039;&amp;#039; — confirmed INBOX/XX-&amp;#039;&amp;#039; naming in place.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Upload sieve/marcus.sieve&amp;#039;&amp;#039;&amp;#039; — uploaded and ACTIVE via ManageSieve (confirmed 2026-02-23). Script: &amp;lt;code&amp;gt;sieve/upload-sieve.py&amp;lt;/code&amp;gt;.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Dedup script written&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;sieve/dedup.py&amp;lt;/code&amp;gt;. Dry-run confirmed 6,622 dupes. Run &amp;lt;code&amp;gt;--execute&amp;lt;/code&amp;gt; to purge.&lt;br /&gt;
* [ ] &amp;#039;&amp;#039;&amp;#039;Run dedup + retroactive sort + empty .Junk&amp;#039;&amp;#039;&amp;#039; — → &amp;lt;code&amp;gt;plans/sieve-dedup.md&amp;lt;/code&amp;gt; (covers all three)&lt;br /&gt;
* [ ] &amp;#039;&amp;#039;&amp;#039;Enable server-wide spam filter&amp;#039;&amp;#039;&amp;#039; — after sieve rules confirmed working in daily use&lt;br /&gt;
&lt;br /&gt;
=== Action needed — Phase 6 (n8n provisioner + per-user relay) ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Create &amp;lt;code&amp;gt;craniumslows-email&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;wilsoz-email&amp;lt;/code&amp;gt; groups&amp;#039;&amp;#039;&amp;#039; in Authentik — done 2026-02-22; marcus in both&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Marcus&amp;#039;s &amp;lt;code&amp;gt;mailAddresses&amp;lt;/code&amp;gt; attribute set&amp;#039;&amp;#039;&amp;#039; — &amp;lt;code&amp;gt;{&amp;quot;mailAddresses&amp;quot;: [&amp;quot;marcus@craniumslows.com&amp;quot;]}&amp;lt;/code&amp;gt; — enables dual-domain delivery in Stalwart&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Build n8n provisioner workflow&amp;#039;&amp;#039;&amp;#039; — Matrix bot IYw0DmCuWEsiV2k9 (deployed 2026-03-01). &amp;lt;code&amp;gt;!provision&amp;lt;/code&amp;gt; in #notifications room, walks through username/name/password/domain conversation. Adaptive polling: 15s during conversation, 5-min throttle when idle.&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Add custom LDAP attributes&amp;#039;&amp;#039;&amp;#039; to Authentik users: &amp;lt;code&amp;gt;mail-name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;mail-alias&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;default-domain&amp;lt;/code&amp;gt; (future, when adding more users) — &amp;#039;&amp;#039;&amp;#039;done 2026-05-10&amp;#039;&amp;#039;&amp;#039;: attributes backfilled on marcus and now written by the provisioner for new users; LDAP property mappings + downstream consumers still deferred&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Configure global MXRoute rate limit&amp;#039;&amp;#039;&amp;#039; — Stalwart queue throttle 400 msg/hr to blizzard.mxrouting.net. → &amp;lt;code&amp;gt;plans/stalwart-helpers-fix.md&amp;lt;/code&amp;gt; (Phase 5) — &amp;#039;&amp;#039;&amp;#039;done 2026-05-10&amp;#039;&amp;#039;&amp;#039;: 400/1h limiter set 2026-05-10 via legacy prefix/values API&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Migrate marcus to per-user relay routes&amp;#039;&amp;#039;&amp;#039; — replace shared craniumslows-mxroute with marcus-specific route — &amp;#039;&amp;#039;&amp;#039;done 2026-05-10&amp;#039;&amp;#039;&amp;#039;: marcus-craniumslows relay + per-sender rule added; shared craniumslows-mxroute kept as fallback&lt;br /&gt;
&lt;br /&gt;
=== Action needed — jkatz@craniumslows.com cleanup ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;jkatz Authentik user&amp;#039;&amp;#039;&amp;#039; — Confirmed deleted (pk:18 not found 2026-05-09). MXRoute account was never created. Consider done.&lt;br /&gt;
&lt;br /&gt;
=== Action needed — Matrix deprovisioner (next session) ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Build Matrix deprovisioner&amp;#039;&amp;#039;&amp;#039; — → &amp;lt;code&amp;gt;plans/email-deprovisioner.md&amp;lt;/code&amp;gt; (depends on &amp;lt;code&amp;gt;plans/stalwart-helpers-fix.md&amp;lt;/code&amp;gt; + &amp;lt;code&amp;gt;plans/email-provisioner.md&amp;lt;/code&amp;gt;) — &amp;#039;&amp;#039;&amp;#039;done 2026-05-10&amp;#039;&amp;#039;&amp;#039;: deployed as jbGL9N1vXT8mB92V, end-to-end tested 2026-05-10&lt;br /&gt;
&lt;br /&gt;
=== Action needed — Provisioner cleanup strategy ===&lt;br /&gt;
* [x] &amp;#039;&amp;#039;&amp;#039;Define orphan .rc file retention policy&amp;#039;&amp;#039;&amp;#039; — &amp;#039;&amp;#039;&amp;#039;done 2026-05-10&amp;#039;&amp;#039;&amp;#039;: policy = move .rc into disabled/ with &amp;lt;code&amp;gt;.deprov-YYYY-MM-DD&amp;lt;/code&amp;gt; suffix on deprovision; weekly cron (&amp;lt;code&amp;gt;/etc/cron.d/getmail-disabled-purge&amp;lt;/code&amp;gt;) purges entries &amp;gt;30 days old. Legacy duplicates + test residue swept.&lt;br /&gt;
&lt;br /&gt;
=== Action needed — Phase 7 (wilsoz.com → MXRoute) ===&lt;br /&gt;
* [ ] Validate craniumslows.com in daily use (give it weeks)&lt;br /&gt;
* [ ] Move wilsoz.com MX DNS → MXRoute&lt;br /&gt;
* [ ] Replace fastmail.rc with wilsoz-marcus.rc (MXRoute fetch)&lt;br /&gt;
* [ ] Update Stalwart wilsoz.com relay → MXRoute smarthost&lt;br /&gt;
* [ ] &amp;#039;&amp;#039;&amp;#039;Flip &amp;lt;code&amp;gt;delete = true&amp;lt;/code&amp;gt;&amp;#039;&amp;#039;&amp;#039; on all getmail configs once cutover verified&lt;br /&gt;
&lt;br /&gt;
=== Minor ===&lt;br /&gt;
* [ ] Investigate unexpected folders in SnappyMail MoveTo dropdown&lt;br /&gt;
&lt;br /&gt;
=== Status (updated 2026-02-22) ===&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Phases 1–4 complete.&amp;#039;&amp;#039;&amp;#039; Stalwart + SnappyMail deployed, LDAP auth working with dual-domain delivery. Maildir import done (137k+ messages). Getmail running: craniumslows 439/439, Fastmail ~5.5k/12.2k in progress. &amp;lt;code&amp;gt;craniumslows-email&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;wilsoz-email&amp;lt;/code&amp;gt; groups created in Authentik. &amp;#039;&amp;#039;&amp;#039;Phase 5 (spam/sieve) and Phase 6 (n8n provisioner) are next.&amp;#039;&amp;#039;&amp;#039; One manual step remaining: add 4 external accounts to SnappyMail UI.&lt;br /&gt;
&lt;br /&gt;
----&lt;/div&gt;</summary>
		<author><name>Wikibot</name></author>
	</entry>
</feed>