Kieran's opinionated (and probably slightly dumb) nix config

feat: add email

dunkirk.sh e08e3aa9 26c3a6e9

verified
Changed files
+130 -34
machines
terebithia
modules
nixos
services
packages
secrets
+19 -3
machines/terebithia/default.nix
··· 477 477 "block-tracking" = { 478 478 name = "Block Player Tracking"; 479 479 description = "Disable real-time player location updates"; 480 - paths = [ "/sse" "/sse/*" "/tiles/*/markers/pl3xmap_players.json" ]; 480 + paths = [ 481 + "/sse" 482 + "/sse/*" 483 + "/tiles/*/markers/pl3xmap_players.json" 484 + ]; 481 485 redact."/tiles/settings.json" = [ "players" ]; 482 486 }; 483 487 }; ··· 489 493 domain = "serif.blue"; 490 494 secretsFile = config.age.secrets.tranquil-pds.path; 491 495 availableUserDomains = [ "serif.blue" ]; 492 - requireInviteCode = true; 493 - 496 + requireInviteCode = false; 497 + 498 + # Email configuration 499 + mail = { 500 + enable = true; 501 + fromAddress = "noreply@serif.blue"; 502 + fromName = "Serif PDS"; 503 + smtp = { 504 + host = "smtp.mailchannels.net"; 505 + port = 587; 506 + username = "kieranklukascontracting"; 507 + }; 508 + }; 509 + 494 510 # Use Backblaze B2 instead of local MinIO 495 511 minio.enable = false; 496 512 s3 = {
+76 -31
modules/nixos/services/tranquil-pds.nix
··· 120 120 default = false; 121 121 description = "Require invite codes for account creation"; 122 122 }; 123 + 124 + mail = { 125 + enable = lib.mkOption { 126 + type = lib.types.bool; 127 + default = false; 128 + description = "Enable email notifications"; 129 + }; 130 + 131 + fromAddress = lib.mkOption { 132 + type = lib.types.str; 133 + default = "noreply@${cfg.domain}"; 134 + description = "Email sender address"; 135 + }; 136 + 137 + fromName = lib.mkOption { 138 + type = lib.types.str; 139 + default = "Serif PDS"; 140 + description = "Email sender name"; 141 + }; 142 + 143 + smtp = { 144 + host = lib.mkOption { 145 + type = lib.types.str; 146 + default = "smtp.mailchannels.net"; 147 + description = "SMTP server hostname"; 148 + }; 149 + 150 + port = lib.mkOption { 151 + type = lib.types.port; 152 + default = 587; 153 + description = "SMTP server port"; 154 + }; 155 + 156 + username = lib.mkOption { 157 + type = lib.types.str; 158 + description = "SMTP username (set in secrets file with SMTP_USERNAME)"; 159 + }; 160 + 161 + tls = lib.mkOption { 162 + type = lib.types.bool; 163 + default = true; 164 + description = "Use STARTTLS"; 165 + }; 166 + }; 167 + }; 123 168 }; 124 169 125 170 config = lib.mkIf cfg.enable { ··· 153 198 rootCredentialsFile = cfg.secretsFile; 154 199 }; 155 200 201 + # Configure msmtp for email sending 202 + programs.msmtp = lib.mkIf cfg.mail.enable { 203 + enable = true; 204 + accounts.default = { 205 + auth = true; 206 + tls = cfg.mail.smtp.tls; 207 + tls_starttls = cfg.mail.smtp.tls; 208 + host = cfg.mail.smtp.host; 209 + port = cfg.mail.smtp.port; 210 + from = cfg.mail.fromAddress; 211 + user = cfg.mail.smtp.username; 212 + passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.secretsFile} | ${pkgs.gnugrep}/bin/grep SMTP_PASSWORD | ${pkgs.coreutils}/bin/cut -d= -f2"; 213 + }; 214 + }; 215 + 156 216 systemd.services.tranquil-pds = { 157 217 description = "Tranquil PDS - AT Protocol Personal Data Server"; 158 218 wantedBy = [ "multi-user.target" ]; ··· 184 244 } 185 245 // lib.optionalAttrs cfg.redis.enable { 186 246 REDIS_URL = "redis://localhost:6379"; 247 + } 248 + // lib.optionalAttrs cfg.mail.enable { 249 + MAIL_FROM_ADDRESS = cfg.mail.fromAddress; 250 + MAIL_FROM_NAME = cfg.mail.fromName; 251 + SENDMAIL_PATH = "${pkgs.msmtp}/bin/msmtp"; 187 252 }; 188 253 189 254 serviceConfig = { ··· 210 275 211 276 services.caddy.virtualHosts."${cfg.domain}" = { 212 277 extraConfig = '' 213 - tls { 214 - dns cloudflare {env.CLOUDFLARE_API_TOKEN} 215 - } 216 - header { 217 - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 218 - } 219 - 220 - # Serve ASCII banner for root path 221 - handle / { 222 - header Content-Type "text/plain; charset=utf-8" 223 - respond ` 224 - _____ ______ _____ _____ ______ ____ _ _ _ ______ 225 - / ____| ____| __ \|_ _| ____| | _ \| | | | | | ____| 226 - | (___ | |__ | |__) | | | | |__ | |_) | | | | | | |__ 227 - \___ \| __| | _ / | | | __| | _ <| | | | | | __| 228 - ____) | |____| | \ \ _| |_| | | |_) | |____| |__| | |____ 229 - |_____/|______|_| \_\_____|_| |____/|______|\____/|______| 230 - 231 - AT Protocol Personal Data Server 232 - 233 - This is a PDS instance running on ${cfg.domain} 234 - 235 - Powered by Tranquil PDS 236 - https://tangled.org/lewis.moe/bspds-sandbox/ 237 - ` 200 238 - } 239 - 240 - reverse_proxy localhost:${toString cfg.port} { 241 - header_up X-Forwarded-Proto {scheme} 242 - header_up X-Forwarded-For {remote} 243 - } 278 + tls { 279 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 280 + } 281 + header { 282 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 283 + } 284 + 285 + reverse_proxy localhost:${toString cfg.port} { 286 + header_up X-Forwarded-Proto {scheme} 287 + header_up X-Forwarded-For {remote} 288 + } 244 289 ''; 245 290 }; 246 291
+35
packages/mailchannels-sendmail.nix
··· 1 + { pkgs, lib, mailchannelsApiKey }: 2 + 3 + pkgs.writeShellScriptBin "mailchannels-sendmail" '' 4 + # Sendmail-compatible wrapper for MailChannels API 5 + # Reads email from stdin and sends via MailChannels 6 + 7 + set -euo pipefail 8 + 9 + # Read the email from stdin 10 + EMAIL_CONTENT=$(cat) 11 + 12 + # Extract headers and body 13 + FROM=$(echo "$EMAIL_CONTENT" | grep -i "^From:" | head -1 | sed 's/^From: //') 14 + TO=$(echo "$EMAIL_CONTENT" | grep -i "^To:" | head -1 | sed 's/^To: //') 15 + SUBJECT=$(echo "$EMAIL_CONTENT" | grep -i "^Subject:" | head -1 | sed 's/^Subject: //') 16 + BODY=$(echo "$EMAIL_CONTENT" | sed -n '/^$/,$p' | tail -n +2) 17 + 18 + # Send via MailChannels API 19 + ${pkgs.curl}/bin/curl -X POST https://api.mailchannels.net/tx/v1/send \ 20 + -H "Content-Type: application/json" \ 21 + -H "X-API-Key: ${mailchannelsApiKey}" \ 22 + -d @- <<EOF 23 + { 24 + "personalizations": [{ 25 + "to": [{"email": "$TO"}] 26 + }], 27 + "from": {"email": "$FROM"}, 28 + "subject": "$SUBJECT", 29 + "content": [{ 30 + "type": "text/plain", 31 + "value": "$BODY" 32 + }] 33 + } 34 + EOF 35 + ''
secrets/tranquil-pds.age

This is a binary file and will not be displayed.