diff --git a/MAIL.md b/MAIL.md new file mode 100644 index 0000000..1c9a251 --- /dev/null +++ b/MAIL.md @@ -0,0 +1,237 @@ +# Mail And Radicale + +This repo currently configures mail for `billenius.com` on `Hermes`. + +## Hosts + +- Mail server: `mail.billenius.com` +- Thunderbird autoconfig: `autoconfig.billenius.com` +- Outlook-style autodiscover: `autodiscover.billenius.com` +- CalDAV/CardDAV via Radicale: `cal.billenius.com` +- Apple configuration profile: `https://autoconfig.billenius.com/mobileconfig/?emailaddress=love%40billenius.com` +- Webmail via Roundcube: `https://mail.billenius.com` + +## Repo Locations + +- Core mail settings: `modules/nixos/mail-server/mail.nix` +- Mail autodiscovery XML: `modules/nixos/mail-server/autodiscover.nix` +- Radicale and DAV discovery: `modules/nixos/mail-server/radicale.nix` +- Roundcube: `modules/nixos/mail-server/roundcube.nix` + +## What The Repo Configures + +### Mail + +- IMAP over SSL on `mail.billenius.com:993` +- SMTP submission with STARTTLS on `mail.billenius.com:587` +- Webmail on `https://mail.billenius.com` +- ACME certificates for the mail-related nginx hosts + +### Autodiscovery + +- Thunderbird mail config at `https://autoconfig.billenius.com/mail/config-v1.1.xml` +- Outlook-style mail config at `https://autodiscover.billenius.com/autodiscover/autodiscover.xml` +- Thunderbird XML also advertises: + - CardDAV: `https://cal.billenius.com/` + - CalDAV: `https://cal.billenius.com/` + +### Radicale + +- Radicale is reverse proxied on `https://cal.billenius.com/` +- Authentication uses the same mail accounts and password hashes as the mail server +- DAV discovery redirects are served on: + - `https://mail.billenius.com/.well-known/caldav` + - `https://mail.billenius.com/.well-known/carddav` + - `https://cal.billenius.com/.well-known/caldav` + - `https://cal.billenius.com/.well-known/carddav` + +This repo does not manage the apex website host for `billenius.com`. If the public website should also expose DAV redirects on `/.well-known/caldav` and `/.well-known/carddav`, that has to be configured on the separate nginx host serving `billenius.com`. + +### Apple Mobileconfig + +- The canonical Apple profile entrypoint is: + - `https://autoconfig.billenius.com/mobileconfig/?emailaddress=` +- Apple profiles are hosted from `autoconfig.billenius.com` +- A small landing page is available at: + - `https://autoconfig.billenius.com/mobileconfig/` +- Account-specific profiles are always available at: + - `https://autoconfig.billenius.com/mobileconfig/.mobileconfig` +- If there is exactly one configured mail account, the profile is also available at: + - `https://autoconfig.billenius.com/mobileconfig/billenius.mobileconfig` + +The profile configures: + +- IMAP on `mail.billenius.com:993` +- SMTP submission on `mail.billenius.com:587` +- CalDAV on `cal.billenius.com:443` +- CardDAV on `cal.billenius.com:443` + +Passwords are not embedded in the profile. + +## DNS + +These records are expected for good client discovery. + +### Host Records + +- `mail.billenius.com` +- `cal.billenius.com` +- `autoconfig.billenius.com` +- `autodiscover.billenius.com` + +`cal.billenius.com`, `autoconfig.billenius.com`, and `autodiscover.billenius.com` can point at the same host as `mail.billenius.com`. + +### Mail SRV + +```dns +_imaps._tcp.billenius.com. SRV 0 0 993 mail.billenius.com. +_submission._tcp.billenius.com. SRV 0 0 587 mail.billenius.com. +``` + +### DAV SRV/TXT + +```dns +_caldavs._tcp.billenius.com. SRV 0 0 443 cal.billenius.com. +_carddavs._tcp.billenius.com. SRV 0 0 443 cal.billenius.com. + +_caldavs._tcp.billenius.com. TXT "path=/" +_carddavs._tcp.billenius.com. TXT "path=/" +``` + +## iOS Setup + +The preferred setup path on Apple devices is to open the hosted profile in Safari: + +```text +https://autoconfig.billenius.com/mobileconfig/?emailaddress=love%40billenius.com +``` + +Install the profile, then enter the password when prompted. + +The manual steps below are the fallback path. + +### Mail + +iOS does not reliably do full self-hosted IMAP setup from the same discovery flow that Thunderbird uses. The hosted `.mobileconfig` profile is the best path for one-step Apple setup. + +1. Open `Settings` +2. Go to `Apps` -> `Mail` -> `Mail Accounts` -> `Add Account` +3. Choose `Other` +4. Choose `Add Mail Account` +5. Use: + - Email: full mail address, for example `love@billenius.com` + - Incoming host: `mail.billenius.com` + - Outgoing host: `mail.billenius.com` + - Username: full mail address + - Password: the mail password +6. iOS should use: + - IMAP SSL on `993` + - SMTP submission on `587` + +### Calendar + +1. Open `Settings` +2. Go to `Apps` -> `Calendar` -> `Calendar Accounts` -> `Add Account` +3. Choose `Other` +4. Choose `Add CalDAV Account` +5. Use: + - Server: `cal.billenius.com` + - Username: full mail address + - Password: the mail password + +### Contacts + +1. Open `Settings` +2. Go to `Apps` -> `Contacts` -> `Contacts Accounts` -> `Add Account` +3. Choose `Other` +4. Choose `Add CardDAV Account` +5. Use: + - Server: `cal.billenius.com` + - Username: full mail address + - Password: the mail password + +If the goal is a single Apple setup flow that provisions mail, calendars, and contacts together, use the hosted `.mobileconfig` profile on `autoconfig.billenius.com` instead of adding the accounts manually. + +## Thunderbird Setup + +### Mail + +Thunderbird should discover mail automatically from the email address alone. + +1. Open Thunderbird +2. Add a new mail account +3. Enter the email address and password +4. Thunderbird should pick up `mail.billenius.com` from `autoconfig.billenius.com` + +### Contacts + +Thunderbird's mail account wizard does not reliably attach CardDAV address books automatically, even when the XML, SRV/TXT records, and `/.well-known` endpoints exist. + +The reliable path is: + +1. Open `Address Book` +2. Choose `New Address Book` -> `Add CardDAV Address Book` +3. Use: + - Username: full mail address + - Location: `https://cal.billenius.com/` +4. Authenticate +5. Select the discovered address books + +### Calendars + +The reliable path is: + +1. Open `Calendar` +2. Choose `New Calendar` +3. Choose `On the Network` +4. Use: + - Username: full mail address + - Location: `https://cal.billenius.com/` +5. Choose `Find Calendars` +6. Subscribe to the discovered calendars + +## Verification + +### Thunderbird XML + +```bash +curl https://autoconfig.billenius.com/mail/config-v1.1.xml +curl https://autodiscover.billenius.com/autodiscover/autodiscover.xml +``` + +### DAV Well-Known + +```bash +curl -I https://mail.billenius.com/.well-known/caldav +curl -I https://mail.billenius.com/.well-known/carddav +curl -I https://cal.billenius.com/.well-known/caldav +curl -I https://cal.billenius.com/.well-known/carddav +``` + +Each should return a `301` redirect to `https://cal.billenius.com/`. + +### DNS + +```bash +dig +short SRV _imaps._tcp.billenius.com +dig +short SRV _submission._tcp.billenius.com +dig +short SRV _caldavs._tcp.billenius.com +dig +short SRV _carddavs._tcp.billenius.com +dig +short TXT _caldavs._tcp.billenius.com +dig +short TXT _carddavs._tcp.billenius.com +``` + +## Current Limitation + +The standards-based discovery in this repo is good enough for: + +- Thunderbird mail autodiscovery +- DAV discovery for many clients +- manual but short setup flows on iOS and Thunderbird + +It is not enough to guarantee that: + +- Thunderbird's mail wizard will also attach calendars and contacts automatically +- iOS will provision mail, contacts, and calendars in one combined login flow + +For that, the next likely improvement is an Apple `mobileconfig` profile and, if needed, client-specific setup documentation. diff --git a/modules/nixos/mail-server/default.nix b/modules/nixos/mail-server/default.nix index 40636dc..2d5f49b 100644 --- a/modules/nixos/mail-server/default.nix +++ b/modules/nixos/mail-server/default.nix @@ -9,6 +9,7 @@ imports = [ ./autodiscover.nix ./mail.nix + ./mobileconfig.nix ./radicale.nix ./roundcube.nix ]; diff --git a/modules/nixos/mail-server/mobileconfig.nix b/modules/nixos/mail-server/mobileconfig.nix new file mode 100644 index 0000000..df06167 --- /dev/null +++ b/modules/nixos/mail-server/mobileconfig.nix @@ -0,0 +1,246 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.mailserver; + hasMailDiscoveryConfig = cfg.enable && cfg.fqdn != null && cfg.domains != [ ]; +in +lib.mkIf hasMailDiscoveryConfig ( + let + domain = builtins.head cfg.domains; + mobileconfigHost = "autoconfig.${domain}"; + radicaleHost = "cal.${domain}"; + safeAccountName = email: builtins.replaceStrings [ "@" "+" ] [ "_" "-" ] email; + + mkUuid = seed: + let + hash = builtins.hashString "sha256" seed; + in + "${lib.substring 0 8 hash}-${lib.substring 8 4 hash}-${lib.substring 12 4 hash}-${lib.substring 16 4 hash}-${lib.substring 20 12 hash}"; + + mkMobileconfig = email: + let + safeName = safeAccountName email; + profileUuid = mkUuid "${email}-apple-profile"; + mailUuid = mkUuid "${email}-apple-mail"; + caldavUuid = mkUuid "${email}-apple-caldav"; + carddavUuid = mkUuid "${email}-apple-carddav"; + in + pkgs.writeText "${safeName}.mobileconfig" '' + + + + + PayloadContent + + + EmailAccountDescription + ${domain} Mail + EmailAccountName + ${email} + EmailAccountType + EmailTypeIMAP + EmailAddress + ${email} + IncomingMailServerAuthentication + EmailAuthPassword + IncomingMailServerHostName + ${cfg.fqdn} + IncomingMailServerPortNumber + 993 + IncomingMailServerUseSSL + + IncomingMailServerUsername + ${email} + OutgoingMailServerAuthentication + EmailAuthPassword + OutgoingMailServerHostName + ${cfg.fqdn} + OutgoingMailServerPortNumber + 587 + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + ${email} + OutgoingPasswordSameAsIncomingPassword + + PayloadDescription + Configures IMAP and SMTP for ${email}. + PayloadDisplayName + Mail + PayloadIdentifier + com.billenius.mobileconfig.${safeName}.mail + PayloadType + com.apple.mail.managed + PayloadUUID + ${mailUuid} + PayloadVersion + 1 + + + CalDAVAccountDescription + ${domain} Calendar + CalDAVHostName + ${radicaleHost} + CalDAVPort + 443 + CalDAVUseSSL + + CalDAVUsername + ${email} + PayloadDescription + Configures CalDAV for ${email}. + PayloadDisplayName + Calendar + PayloadIdentifier + com.billenius.mobileconfig.${safeName}.caldav + PayloadType + com.apple.caldav.account + PayloadUUID + ${caldavUuid} + PayloadVersion + 1 + + + CardDAVAccountDescription + ${domain} Contacts + CardDAVHostName + ${radicaleHost} + CardDAVPort + 443 + CardDAVUseSSL + + CardDAVUsername + ${email} + PayloadDescription + Configures CardDAV for ${email}. + PayloadDisplayName + Contacts + PayloadIdentifier + com.billenius.mobileconfig.${safeName}.carddav + PayloadType + com.apple.carddav.account + PayloadUUID + ${carddavUuid} + PayloadVersion + 1 + + + PayloadDescription + Configures mail, calendar, and contacts for ${email}. + PayloadDisplayName + ${domain} Mail + PayloadIdentifier + com.billenius.mobileconfig.${safeName} + PayloadOrganization + ${domain} + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + ${profileUuid} + PayloadVersion + 1 + + + ''; + + mobileconfigProfiles = lib.mapAttrs ( + email: _: { + inherit email; + safeName = safeAccountName email; + profile = mkMobileconfig email; + } + ) cfg.loginAccounts; + + mobileconfigLandingPage = pkgs.writeText "mobileconfig-index.html" '' + + + + + + ${domain} Apple Profile + + +

${domain} Apple Profile

+

Open this page in Safari on iPhone, iPad, or macOS and enter your email address to download the configuration profile.

+
+ + + +
+

The profile configures IMAP, SMTP, CalDAV, and CardDAV. You will still be asked for your password during installation.

+ + + ''; + + mobileconfigRedirectConfig = + lib.concatStrings ( + lib.mapAttrsToList ( + email: profile: '' + if ($arg_emailaddress = "${email}") { + return 302 /mobileconfig/${profile.safeName}.mobileconfig; + } + '' + ) mobileconfigProfiles + ); + + mobileconfigLocations = lib.listToAttrs ( + [ + { + name = "= /mobileconfig"; + value.return = "308 /mobileconfig/"; + } + { + name = "= /mobileconfig/"; + value.extraConfig = '' + ${mobileconfigRedirectConfig} + return 302 /mobileconfig/index.html; + ''; + } + { + name = "= /mobileconfig/index.html"; + value.extraConfig = '' + default_type text/html; + alias ${mobileconfigLandingPage}; + ''; + } + ] + ++ lib.mapAttrsToList ( + _: profile: { + name = "= /mobileconfig/${profile.safeName}.mobileconfig"; + value.extraConfig = '' + default_type application/x-apple-aspen-config; + add_header Content-Disposition 'attachment; filename="${profile.safeName}.mobileconfig"' always; + alias ${profile.profile}; + ''; + } + ) mobileconfigProfiles + ); + + defaultMobileconfigLocation = + let + accounts = lib.attrNames mobileconfigProfiles; + in + if lib.length accounts == 1 then + let + profile = mobileconfigProfiles.${builtins.head accounts}; + in + { + "= /mobileconfig/billenius.mobileconfig".extraConfig = '' + default_type application/x-apple-aspen-config; + add_header Content-Disposition 'attachment; filename="billenius.mobileconfig"' always; + alias ${profile.profile}; + ''; + } + else + { }; + in + { + services.nginx.virtualHosts.${mobileconfigHost}.locations = mobileconfigLocations // defaultMobileconfigLocation; + } +)