about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--mail.nix176
1 files changed, 155 insertions, 21 deletions
diff --git a/mail.nix b/mail.nix
index 61b80c1..505210e 100644
--- a/mail.nix
+++ b/mail.nix
@@ -1,5 +1,5 @@
 # Email server configuration
-# Copyright (C) 2023  Nguyễn Gia Phong
+# Copyright (C) 2023-2024  Nguyễn Gia Phong
 #
 # This file is part of loang configuration.
 #
@@ -16,11 +16,12 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with loang configuration.  If not, see <https://www.gnu.org/licenses/>.
 
-{ config, options, pkgs, ... }:
+{ config, lib, pkgs, ... }:
 let
   certDir = config.security.acme.certs.${hostname}.directory;
   domain = config.networking.domain;
   hostname = "tem.${domain}";
+  publicHost = "chung.${domain}";
 in {
   networking.firewall.allowedTCPPorts = [
     25 # SMTP-MTA
@@ -38,31 +39,130 @@ in {
     maddy = {
       config = ''
         auth_map email_localpart
-      '' + (builtins.replaceStrings [
-        ''
-          auth.pass_table local_authdb {
-            table sql_table {
-              driver sqlite3
-              dsn credentials.db
-              table_name passwords
+
+        auth.shadow local_authdb {
+          use_helper no
+        }
+
+        storage.imapsql local_mailboxes {
+          driver sqlite3
+          dsn imapsql.db
+        }
+
+        table.chain local_rewrites {
+          optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
+          optional_step static {
+            entry postmaster cnx@$(primary_domain)
+          }
+          optional_step file /etc/maddy/aliases
+        }
+
+        msgpipeline local_routing {
+          destination ${publicHost} {
+            deliver_to smtp tcp://localhost:2525
+          }
+          destination postmaster $(local_domains) {
+            modify {
+              replace_rcpt &local_rewrites
+            }
+            deliver_to &local_mailboxes
+          }
+          default_destination {
+            reject 550 5.1.1 "User doesn't exist"
+          }
+        }
+
+        smtp tcp://0.0.0.0:25 {
+          limits {
+            all rate 20 1s
+            all concurrency 10
+          }
+          dmarc yes
+          check {
+            require_mx_record
+            dkim
+            spf
+          }
+          source $(local_domains) {
+            reject 501 5.1.8 "Use Submission for outgoing SMTP"
+          }
+          default_source {
+            destination postmaster $(local_domains) {
+              deliver_to &local_routing
+            }
+            default_destination {
+              reject 550 5.1.1 "User doesn't exist"
+            }
+          }
+        }
+
+        submission tls://0.0.0.0:465 {
+          limits {
+            all rate 50 1s
+          }
+          auth &local_authdb
+          source $(local_domains) {
+            check {
+              authorize_sender {
+                prepare_email &local_rewrites
+                user_to_email identity
+              }
+            }
+            destination postmaster $(local_domains) {
+              deliver_to &local_routing
+            }
+            default_destination {
+              modify {
+                dkim $(primary_domain) $(local_domains) default
+              }
+              deliver_to &remote_queue
+            }
+          }
+          default_source {
+            reject 501 5.1.8 "Non-local sender domain"
+          }
+        }
+
+        target.remote outbound_delivery {
+          limits {
+            destination rate 20 1s
+            destination concurrency 10
+          }
+          mx_auth {
+            dane
+            mtasts {
+              cache fs
+              fs_dir mtasts_cache/
+            }
+            local_policy {
+              min_tls_level encrypted
+              min_mx_level none
             }
           }
-        ''
-        "imap tcp://0.0.0.0:143"
-        "submission tcp://0.0.0.0:587"
-      ] [
-        ''
-          auth.shadow local_authdb {
-            debug yes
-            use_helper no
+        }
+
+        target.queue remote_queue {
+          target &outbound_delivery
+          autogenerated_msg_domain $(primary_domain)
+          bounce {
+            destination postmaster $(local_domains) {
+              deliver_to &local_routing
+            }
+            default_destination {
+              reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
+            }
           }
-        ''
-        "imap tls://0.0.0.0:993"
-        "submission tls://0.0.0.0:465"
-      ] options.services.maddy.config.default);
+        }
+
+        imap tls://0.0.0.0:993 {
+          auth &local_authdb
+          storage &local_mailboxes
+        }
+      '';
       enable = true;
       hostname = hostname;
       primaryDomain = domain;
+      localDomains = [ domain publicHost ];
       tls = {
         loader = "file";
         certificates = [{
@@ -72,6 +172,32 @@ in {
       };
     };
 
+    postfix = {
+      enable = true; # bridge between maddy and public-inbox
+      enableSmtp = false; # override default port 25
+      extraMasterConf = ''
+        2525          inet  n        -       -       -        -        smtpd
+      '';
+    };
+
+    public-inbox = {
+      enable = true;
+      http = {
+        enable = true;
+        port = 1430;
+      };
+      inboxes = {
+        test = {
+          address = [ "test@${publicHost}" ];
+          description = "test list";
+          url = "https://${publicHost}/test";
+        };
+      };
+      mda.enable = true;
+      postfix.enable = true;
+      settings.publicinbox.wwwlisting = "match=domain";
+    };
+
     nginx.virtualHosts = {
       "mta-sts.${domain}" = {
         enableACME = true;
@@ -109,6 +235,14 @@ in {
         '';
       };
 
+      "${publicHost}" = {
+        enableACME = true;
+        forceSSL = true;
+        locations."/".proxyPass = let
+          port = config.services.public-inbox.http.port;
+        in "http://localhost:${toString port}";
+      };
+
       ${hostname} = let alps = config.services.alps;
       in {
         enableACME = true;