]> git.immae.eu Git - perso/Immae/Config/Nix.git/commitdiff
Configure mail (dovecot, postfix, spam checks)
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Sun, 23 Jun 2019 19:04:55 +0000 (21:04 +0200)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Sat, 29 Jun 2019 13:14:43 +0000 (15:14 +0200)
19 files changed:
modules/myids.nix
modules/private/default.nix
modules/private/dns.nix
modules/private/mail/default.nix [new file with mode: 0644]
modules/private/mail/dovecot.nix [new file with mode: 0644]
modules/private/mail/milters.nix [moved from modules/private/mail.nix with 83% similarity]
modules/private/mail/postfix.nix [new file with mode: 0644]
modules/private/mail/rspamd.nix [new file with mode: 0644]
modules/private/mail/scan_reported_mails [new file with mode: 0755]
modules/private/mail/sieve_bin/imapsieve_copy [new file with mode: 0755]
modules/private/mail/sieve_scripts/report_ham.sieve [new file with mode: 0644]
modules/private/mail/sieve_scripts/report_spam.sieve [new file with mode: 0644]
modules/private/websites/tools/tools/roundcubemail.nix
pkgs/default.nix
pkgs/dovecot/plugins/deleted_to_trash/default.nix [new file with mode: 0644]
pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json [new file with mode: 0644]
pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch [new file with mode: 0644]
pkgs/dovecot/plugins/fts_xapian/default.nix [new file with mode: 0644]
pkgs/dovecot/plugins/fts_xapian/fts-xapian.json [new file with mode: 0644]

index 7ec9c0efc5e595d7f591f3ed6ce1c9c851692a05..e949ca7ae8651ee09bb26aab85c66c43a0fa3b7f 100644 (file)
@@ -3,7 +3,8 @@
   # Check that there is no clash with nixos/modules/misc/ids.nix
   config = {
     ids.uids = {
-      opendarc = 391;
+      vhost = 390;
+      openarc = 391;
       opendmarc = 392;
       peertube = 394;
       redis = 395;
@@ -13,7 +14,8 @@
       mastodon = 399;
     };
     ids.gids = {
-      opendarc = 392;
+      vhost = 390;
+      openarc = 391;
       opendmarc = 392;
       peertube = 394;
       redis = 395;
index 894efb761ca75bda24063975b603cb63d8487e58..026e69d5a6f4751256b7e463602b388572925535 100644 (file)
@@ -47,6 +47,12 @@ set = {
   peertubeTool = ./websites/tools/peertube;
   toolsTool = ./websites/tools/tools;
 
+  mail = ./mail;
+  mailMilters = ./mail/milters.nix;
+  mailPostfix = ./mail/postfix.nix;
+  mailDovecot = ./mail/dovecot.nix;
+  mailRspamd = ./mail/rspamd.nix;
+
   buildbot = ./buildbot;
   certificates = ./certificates.nix;
   gitolite = ./gitolite;
@@ -55,7 +61,6 @@ set = {
   tasks = ./tasks;
   dns = ./dns.nix;
   ftp = ./ftp.nix;
-  mail = ./mail.nix;
   mpd = ./mpd.nix;
   ssh = ./ssh;
 
index f12f9822664ad46361e6c9e66ef761295b5ea9b7..6647c1428c4bc735c436e695f05e2d201fd4c94e 100644 (file)
               ''
               ; ------------------ mail: ${n} ---------------------------
               ${n} IN MX 10 mail.${conf.name}.
-              ;${n} IN MX 50 mx-1.${conf.name}.
+              ${n} IN MX 50 mx-1.${conf.name}.
 
               ; https://tools.ietf.org/html/rfc6186
               _submission._tcp${suffix} SRV  0 1  587 smtp.immae.eu.
diff --git a/modules/private/mail/default.nix b/modules/private/mail/default.nix
new file mode 100644 (file)
index 0000000..ad2c684
--- /dev/null
@@ -0,0 +1,12 @@
+{ lib, pkgs, config, myconfig,  ... }:
+{
+  config.security.acme.certs."mail" = config.services.myCertificates.certConfig // {
+    domain = "eldiron.immae.eu";
+    extraDomains = let
+      zonesWithMx = builtins.filter (zone:
+        lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
+      ) myconfig.env.dns.masterZones;
+      mxs = map (zone: "mx-1.${zone.name}") zonesWithMx;
+    in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
+  };
+}
diff --git a/modules/private/mail/dovecot.nix b/modules/private/mail/dovecot.nix
new file mode 100644 (file)
index 0000000..d757f59
--- /dev/null
@@ -0,0 +1,255 @@
+{ lib, pkgs, config, myconfig,  ... }:
+let
+  sieve_bin = pkgs.runCommand "sieve_bin" {
+    buildInputs = [ pkgs.makeWrapper ];
+  } ''
+    cp -a ${./sieve_bin} $out
+    chmod -R u+w $out
+    patchShebangs $out
+    for i in $out/*; do
+      wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils ]}
+    done
+    '';
+in
+{
+  config.secrets.keys = [
+    {
+      dest = "dovecot/ldap";
+      user = config.services.dovecot2.user;
+      group = config.services.dovecot2.group;
+      permissions = "0400";
+      text = ''
+        hosts = ${myconfig.env.mail.dovecot.ldap.host}
+        tls = yes
+
+        dn = ${myconfig.env.mail.dovecot.ldap.dn}
+        dnpass = ${myconfig.env.mail.dovecot.ldap.password}
+
+        auth_bind = yes
+
+        ldap_version = 3
+
+        base = ${myconfig.env.mail.dovecot.ldap.base}
+        scope = subtree
+
+        user_filter = ${myconfig.env.mail.dovecot.ldap.filter}
+        pass_filter = ${myconfig.env.mail.dovecot.ldap.filter}
+
+        user_attrs = ${myconfig.env.mail.dovecot.ldap.user_attrs}
+        pass_attrs = ${myconfig.env.mail.dovecot.ldap.pass_attrs}
+        '';
+    }
+  ];
+
+  config.users.users.vhost = {
+    group = "vhost";
+    uid = config.ids.uids.vhost;
+  };
+  config.users.groups.vhost.gid = config.ids.gids.vhost;
+
+  # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous.......
+  config.services.dovecot2 = {
+    enable = true;
+    enablePAM = false;
+    enablePop3 = true;
+    enableImap = true;
+    enableLmtp = true;
+    protocols = [ "sieve" ];
+    modules = [
+      pkgs.dovecot_pigeonhole
+      pkgs.dovecot_deleted-to-trash
+      pkgs.dovecot_fts-xapian
+    ];
+    mailUser = "vhost";
+    mailGroup = "vhost";
+    createMailUser = false;
+    mailboxes = [
+      { name = "Trash";  auto = "subscribe"; specialUse = "Trash"; }
+      { name = "Junk";   auto = "subscribe"; specialUse = "Junk"; }
+      { name = "Sent";   auto = "subscribe"; specialUse = "Sent"; }
+      { name = "Drafts"; auto = "subscribe"; specialUse = "Drafts"; }
+    ];
+    mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap";
+    sslServerCert = "/var/lib/acme/mail/fullchain.pem";
+    sslServerKey = "/var/lib/acme/mail/key.pem";
+    sslCACert = "/var/lib/acme/mail/fullchain.pem";
+    extraConfig = builtins.concatStringsSep "\n" [
+      ''
+        postmaster_address = postmaster@immae.eu
+        mail_attribute_dict = file:%h/dovecot-attributes
+        imap_idle_notify_interval = 20 mins
+        namespace inbox {
+          type = private
+          separator = /
+          inbox = yes
+          list = yes
+        }
+      ''
+
+      # Full text search
+      ''
+        # needs to be bigger than any mailbox size
+        default_vsz_limit = 2GB
+        mail_plugins = $mail_plugins fts fts_xapian
+        plugin {
+          plugin = fts fts_xapian
+          fts = xapian
+          fts_xapian = partial=2 full=20
+          fts_autoindex = yes
+          fts_autoindex_exclude = \Junk
+          fts_autoindex_exclude2 = \Trash
+          fts_autoindex_exclude3 = Virtual/*
+        }
+      ''
+
+      # Antispam
+      # https://docs.iredmail.org/dovecot.imapsieve.html
+      ''
+      # imap_sieve plugin added below
+
+      plugin {
+          sieve_plugins = sieve_imapsieve sieve_extprograms
+          imapsieve_url = sieve://127.0.0.1:4190
+
+          # From elsewhere to Junk folder
+          imapsieve_mailbox1_name = Junk
+          imapsieve_mailbox1_causes = COPY APPEND
+          imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin
+
+          # From Junk folder to elsewhere
+          imapsieve_mailbox2_name = *
+          imapsieve_mailbox2_from = Junk
+          imapsieve_mailbox2_causes = COPY
+          imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin
+
+          sieve_pipe_bin_dir = ${sieve_bin}
+
+          sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
+      }
+      ''
+      # Services to listen
+      ''
+      service imap-login {
+        inet_listener imap {
+        }
+        inet_listener imaps {
+        }
+      }
+      service pop3-login {
+        inet_listener pop3 {
+        }
+        inet_listener pop3s {
+        }
+      }
+      service imap {
+      }
+      service pop3 {
+      }
+      service auth {
+        unix_listener auth-userdb {
+        }
+        unix_listener ${config.services.postfix.config.queue_directory}/private/auth {
+          mode = 0666
+        }
+      }
+      service auth-worker {
+      }
+      service dict {
+        unix_listener dict {
+        }
+      }
+      service stats {
+        unix_listener stats-reader {
+          user = vhost
+          group = vhost
+          mode = 0660
+        }
+        unix_listener stats-writer {
+          user = vhost
+          group = vhost
+          mode = 0660
+        }
+      }
+      ''
+
+      # Authentification
+      ''
+      first_valid_uid = ${toString config.ids.uids.vhost}
+      disable_plaintext_auth = yes
+      passdb {
+        driver = ldap
+        args = ${config.secrets.fullPaths."dovecot/ldap"}
+      }
+      userdb {
+        driver = static
+        args = user=%u uid=vhost gid=vhost home=/var/lib/vhost/%d/%n/ mail=mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap
+      }
+      ''
+
+      # Zlib
+      ''
+      mail_plugins = $mail_plugins zlib
+      plugin {
+        zlib_save_level = 6
+        zlib_save = gz
+      }
+      ''
+
+      # Sieve
+      ''
+      plugin {
+        sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve
+      }
+      service managesieve-login {
+      }
+      service managesieve {
+      }
+      ''
+
+      # Deleted to trash
+      ''
+      plugin {
+        deleted_to_trash_folder = Trash
+      }
+      ''
+
+      # Virtual mailboxes
+      ''
+      mail_plugins = $mail_plugins virtual
+      namespace Virtual {
+        prefix = Virtual/
+        location = virtual:~/Virtual
+      }
+      ''
+
+      # Protocol specific configuration
+      # Needs to come last if there are mail_plugins entries
+      ''
+      protocol imap {
+        mail_plugins = $mail_plugins deleted_to_trash imap_sieve
+      }
+      protocol lda {
+        mail_plugins = $mail_plugins sieve
+      }
+      ''
+    ];
+  };
+  config.networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ];
+  config.system.activationScripts.dovecot = {
+    deps = [ "users" ];
+    text  =''
+      install -m 0755 -o vhost -g vhost -d /var/lib/vhost
+      '';
+  };
+
+  config.security.acme.certs."mail" = {
+    postRun = ''
+      systemctl restart dovecot2.service
+    '';
+    extraDomains = {
+      "imap.immae.eu" = null;
+      "pop3.immae.eu" = null;
+    };
+  };
+}
+
similarity index 83%
rename from modules/private/mail.nix
rename to modules/private/mail/milters.nix
index eb869ba3eb28e9100d562761880f3c277cd00650..c4bd990b2766a3e89fa3d914669663f2a520bcc5 100644 (file)
@@ -1,16 +1,17 @@
 { lib, pkgs, config, myconfig,  ... }:
 {
-  config.users.users.nullmailer.uid = config.ids.uids.nullmailer;
-  config.users.groups.nullmailer.gid = config.ids.gids.nullmailer;
-
-  config.services.nullmailer = {
-    enable = true;
-    config = {
-      me = myconfig.env.mail.host;
-      remotes = "${myconfig.env.mail.relay} smtp";
+  options.myServices.mail.milters.sockets = lib.mkOption {
+    type = lib.types.attrsOf lib.types.path;
+    default = {
+      opendkim = "/run/opendkim/opendkim.sock";
+      opendmarc = "/run/opendmarc/opendmarc.sock";
+      openarc = "/run/openarc/openarc.sock";
     };
+    readOnly = true;
+    description = ''
+      milters sockets
+      '';
   };
-
   config.secrets.keys = [
     {
       dest = "opendkim/eldiron.private";
@@ -38,6 +39,7 @@
   config.users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ];
   config.services.opendkim = {
     enable = true;
+    socket = "local:${config.myServices.mail.milters.sockets.opendkim}";
     domains = builtins.concatStringsSep "," (lib.flatten (map
       (zone: map
         (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}")
@@ -51,6 +53,7 @@
       SubDomains     yes
       UMask          002
       '';
+    group = config.services.postfix.group;
   };
   config.systemd.services.opendkim.preStart = lib.mkBefore ''
     # Skip the prestart script as keys are handled in secrets
@@ -66,6 +69,7 @@
   config.users.users."${config.services.opendmarc.user}".extraGroups = [ "keys" ];
   config.services.opendmarc = {
     enable = true;
+    socket = "local:${config.myServices.mail.milters.sockets.opendmarc}";
     configFile = pkgs.writeText "opendmarc.conf" ''
       AuthservID                  HOSTNAME
       FailureReports              false
@@ -79,6 +83,7 @@
       TrustedAuthservIDs          HOSTNAME, immae.eu, nef2.ens.fr
       UMask                       002
       '';
+    group = config.services.postfix.group;
   };
   config.services.filesWatcher.opendmarc = {
     restart = true;
@@ -90,7 +95,8 @@
   config.services.openarc = {
     enable = true;
     user = "opendkim";
-    group = "opendkim";
+    socket = "local:${config.myServices.mail.milters.sockets.openarc}";
+    group = config.services.postfix.group;
     configFile = pkgs.writeText "openarc.conf" ''
       AuthservID              mail.immae.eu
       Domain                  mail.immae.eu
diff --git a/modules/private/mail/postfix.nix b/modules/private/mail/postfix.nix
new file mode 100644 (file)
index 0000000..53bf650
--- /dev/null
@@ -0,0 +1,227 @@
+{ lib, pkgs, config, myconfig,  ... }:
+{
+  config.secrets.keys = [
+    {
+      dest = "postfix/mysql_alias_maps";
+      user = config.services.postfix.user;
+      group = config.services.postfix.group;
+      permissions = "0440";
+      text = ''
+        # We need to specify that option to trigger ssl connection
+        tls_ciphers = TLSv1.2
+        user = ${myconfig.env.mail.postfix.mysql.user}
+        password = ${myconfig.env.mail.postfix.mysql.password}
+        hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
+        dbname = ${myconfig.env.mail.postfix.mysql.database}
+        query = SELECT DISTINCT destination
+          FROM forwardings_merge
+          WHERE
+            ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
+            AND active = 1
+            AND '%s' NOT IN
+            (
+              SELECT source
+              FROM forwardings_blacklisted
+              WHERE source = '%s'
+            ) UNION
+            SELECT 'devnull@immae.eu'
+            FROM forwardings_blacklisted
+            WHERE source = '%s'
+        '';
+    }
+    {
+      dest = "postfix/mysql_mailbox_maps";
+      user = config.services.postfix.user;
+      group = config.services.postfix.group;
+      permissions = "0440";
+      text = ''
+        # We need to specify that option to trigger ssl connection
+        tls_ciphers = TLSv1.2
+        user = ${myconfig.env.mail.postfix.mysql.user}
+        password = ${myconfig.env.mail.postfix.mysql.password}
+        hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
+        dbname = ${myconfig.env.mail.postfix.mysql.database}
+        result_format = /%d/%u
+        query = SELECT DISTINCT '%s'
+          FROM mailboxes
+          WHERE active = 1
+          AND (
+            (domain = '%d' AND user = '%u' AND regex = 0)
+            OR (
+              regex = 1
+              AND '%d' REGEXP CONCAT('^',domain,'$')
+              AND '%u' REGEXP CONCAT('^',user,'$')
+            )
+          )
+          LIMIT 1
+      '';
+    }
+    {
+      dest = "postfix/mysql_sender_login_maps";
+      user = config.services.postfix.user;
+      group = config.services.postfix.group;
+      permissions = "0440";
+      text = ''
+        # We need to specify that option to trigger ssl connection
+        tls_ciphers = TLSv1.2
+        user = ${myconfig.env.mail.postfix.mysql.user}
+        password = ${myconfig.env.mail.postfix.mysql.password}
+        hosts = unix:${myconfig.env.mail.postfix.mysql.socket}
+        dbname = ${myconfig.env.mail.postfix.mysql.database}
+        query = SELECT DISTINCT destination
+          FROM forwardings_merge
+          WHERE
+            ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
+            AND active = 1
+        '';
+    }
+  ];
+
+  config.networking.firewall.allowedTCPPorts = [ 25 587 ];
+
+  config.nixpkgs.overlays = [ (self: super: {
+    postfix = super.postfix.override { withMySQL = true; };
+  }) ];
+  config.users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
+  config.services.filesWatcher.postfix = {
+    restart = true;
+    paths = [
+      config.secrets.fullPaths."postfix/mysql_alias_maps"
+      config.secrets.fullPaths."postfix/mysql_mailbox_maps"
+      config.secrets.fullPaths."postfix/mysql_sender_login_maps"
+    ];
+  };
+  config.services.postfix = {
+    mapFiles = let
+      name = n: i: "relay_${n}_${toString i}";
+      pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
+        if m.type == "hash"
+        then pkgs.writeText (name n i) m.content
+        else null
+      );
+      pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
+    in
+      lib.attrsets.filterAttrs (k: v: v != null) (
+        lib.attrsets.listToAttrs (lib.flatten (
+          lib.attrsets.mapAttrsToList pairs myconfig.env.mail.postfix.backup_domains
+        ))
+      );
+    config = {
+      ### postfix module overrides
+      readme_directory = "${pkgs.postfix}/share/postfix/doc";
+      smtp_tls_CAfile = lib.mkForce "";
+      smtp_tls_cert_file = lib.mkForce "";
+      smtp_tls_key_file = lib.mkForce "";
+
+      message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
+      alias_database = "\$alias_maps";
+
+      ### Virtual mailboxes config
+      virtual_alias_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}";
+      virtual_mailbox_domains = myconfig.env.mail.postfix.additional_mailbox_domains
+       ++ lib.remove "localhost.immae.eu" (lib.remove null (lib.flatten (map
+          (zone: map
+            (e: if e.receive
+            then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
+            else null
+            )
+            (zone.withEmail or [])
+          )
+          myconfig.env.dns.masterZones
+        )));
+      virtual_mailbox_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_mailbox_maps"}";
+      dovecot_destination_recipient_limit = "1";
+      virtual_transport = "dovecot";
+
+      ### Relay domains
+      relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) myconfig.env.mail.postfix.backup_domains);
+      relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
+        lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
+      ) myconfig.env.mail.postfix.backup_domains);
+
+      ### Additional smtpd configuration
+      smtpd_tls_received_header = "yes";
+      smtpd_tls_loglevel = "1";
+
+      ### Email sending configuration
+      smtp_tls_security_level = "may";
+      smtp_tls_loglevel = "1";
+
+      ### Force ip bind for smtp
+      smtp_bind_address  = myconfig.env.servers.eldiron.ips.main.ip4;
+      smtp_bind_address6 = builtins.head myconfig.env.servers.eldiron.ips.main.ip6;
+
+      # #Unneeded if postfix can only send e-mail from "self" domains
+      # #smtp_sasl_auth_enable = "yes";
+      # #smtp_sasl_password_maps = "hash:/etc/postfix/relay_creds";
+      # #smtp_sasl_security_options = "noanonymous";
+      # #smtp_sender_dependent_authentication = "yes";
+      # #sender_dependent_relayhost_maps = "hash:/etc/postfix/sender_relay";
+
+      ### opendkim, opendmarc, openarc milters
+      non_smtpd_milters = [
+        "unix:${config.myServices.mail.milters.sockets.opendkim}"
+        "unix:${config.myServices.mail.milters.sockets.opendmarc}"
+        "unix:${config.myServices.mail.milters.sockets.openarc}"
+      ];
+      smtpd_milters = [
+        "unix:${config.myServices.mail.milters.sockets.opendkim}"
+        "unix:${config.myServices.mail.milters.sockets.opendmarc}"
+        "unix:${config.myServices.mail.milters.sockets.openarc}"
+      ];
+    };
+    enable = true;
+    enableSmtp = true;
+    enableSubmission = true;
+    submissionOptions = {
+      smtpd_tls_security_level = "encrypt";
+      smtpd_sasl_auth_enable = "yes";
+      smtpd_tls_auth_only = "yes";
+      smtpd_sasl_tls_security_options = "noanonymous";
+      smtpd_sasl_type = "dovecot";
+      smtpd_sasl_path = "private/auth";
+      smtpd_reject_unlisted_recipient = "no";
+      smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+      # Refuse to send e-mails with a From that is not handled
+      smtpd_sender_restrictions =
+        "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject";
+      smtpd_sender_login_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}";
+      smtpd_recipient_restrictions = "permit_sasl_authenticated,reject";
+      milter_macro_daemon_name = "ORIGINATING";
+      smtpd_milters = "unix:${config.myServices.mail.milters.sockets.opendkim}";
+    };
+    destination = ["localhost"];
+    # This needs to reverse DNS
+    hostname = "eldiron.immae.eu";
+    setSendmail = true;
+    sslCert = "/var/lib/acme/mail/fullchain.pem";
+    sslKey = "/var/lib/acme/mail/key.pem";
+    recipientDelimiter = "+";
+    masterConfig = {
+      dovecot = {
+        type = "unix";
+        privileged = true;
+        chroot = false;
+        command = "pipe";
+        args = let
+          # rspamd could be used as a milter, but then it cannot apply
+          # its checks "per user" (milter is not yet dispatched to
+          # users), so we wrap dovecot-lda inside rspamc per recipient
+          # here.
+          dovecot_exe = "${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f \${sender} -a \${original_recipient} -d \${user}@\${nexthop}";
+        in [
+          "flags=DRhu" "user=vhost:vhost"
+          "argv=${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d \${user}@\${nexthop} --mime --exec {${dovecot_exe}}"
+        ];
+      };
+    };
+  };
+  config.security.acme.certs."mail" = {
+    postRun = ''
+      systemctl restart postfix.service
+      '';
+    extraDomains = {
+      "smtp.immae.eu" = null;
+    };
+  };
+}
diff --git a/modules/private/mail/rspamd.nix b/modules/private/mail/rspamd.nix
new file mode 100644 (file)
index 0000000..3a7a67c
--- /dev/null
@@ -0,0 +1,84 @@
+{ lib, pkgs, config, myconfig,  ... }:
+{
+  options.myServices.mail.rspamd.sockets = lib.mkOption {
+    type = lib.types.attrsOf lib.types.path;
+    default = {
+      worker-controller = "/run/rspamd/worker-controller.sock";
+    };
+    readOnly = true;
+    description = ''
+      rspamd sockets
+      '';
+  };
+  config.services.cron.systemCronJobs = let
+    cron_script = pkgs.runCommand "cron_script" {
+      buildInputs = [ pkgs.makeWrapper ];
+    } ''
+      mkdir -p $out
+      cp ${./scan_reported_mails} $out/scan_reported_mails
+      patchShebangs $out
+      for i in $out/*; do
+        wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.rspamd pkgs.flock ]}
+      done
+      '';
+  in
+    [ "*/20 * * * * vhost ${cron_script}/scan_reported_mails" ];
+
+  config.services.rspamd = {
+    enable = true;
+    debug = true;
+    overrides = {
+      "actions.conf".text = ''
+        reject = null;
+        add_header = 6;
+        greylist = null;
+        '';
+      "milter_headers.conf".text = ''
+        extended_spam_headers = true;
+      '';
+    };
+    locals = {
+      "redis.conf".text = ''
+        servers = "${myconfig.env.mail.rspamd.redis.socket}";
+        db = "${myconfig.env.mail.rspamd.redis.db}";
+        '';
+      "classifier-bayes.conf".text = ''
+        users_enabled = true;
+        backend = "redis";
+        servers = "${myconfig.env.mail.rspamd.redis.socket}";
+        database = "${myconfig.env.mail.rspamd.redis.db}";
+        autolearn = true;
+        cache {
+          backend = "redis";
+        }
+        new_schema = true;
+        statfile {
+          BAYES_HAM {
+            spam = false;
+          }
+          BAYES_SPAM {
+            spam = true;
+          }
+        }
+        '';
+    };
+    workers = {
+      controller = {
+        extraConfig = ''
+          enable_password = "${myconfig.env.mail.rspamd.write_password_hashed}";
+          password = "${myconfig.env.mail.rspamd.read_password_hashed}";
+        '';
+        bindSockets = [ {
+          socket = config.myServices.mail.rspamd.sockets.worker-controller;
+          mode = "0660";
+          owner = config.services.rspamd.user;
+          group = "vhost";
+        } ];
+      };
+    };
+    postfix = {
+      enable = true;
+      config = {};
+    };
+  };
+}
diff --git a/modules/private/mail/scan_reported_mails b/modules/private/mail/scan_reported_mails
new file mode 100755 (executable)
index 0000000..fe9f4d6
--- /dev/null
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+( flock -n 9 || exit 1
+shopt -s nullglob
+for spool in /var/lib/vhost/.rspamd/*/pending; do
+  rspamd_folder=$(dirname $spool)
+  mail_user=$(basename $rspamd_folder)
+  mv $rspamd_folder/pending $rspamd_folder/processing
+
+  for mtype in ham spam; do
+    if [ -d $rspamd_folder/processing/$mtype ]; then
+      output="$(rspamc -h /run/rspamd/worker-controller.sock -c bayes -d $mail_user learn_$mtype $rspamd_folder/processing/$mtype/*)"
+      echo "[$mtype: $mail_user]" ${output} >> /var/lib/vhost/.rspamd/rspamd.log
+      mkdir -p $rspamd_folder/processed/$mtype
+      cp $rspamd_folder/processing/$mtype/* $rspamd_folder/processed/$mtype/
+    fi
+  done
+
+  rm -rf $rspamd_folder/processing
+done
+) 9>/var/lib/vhost/scan_reported_mails.lock
diff --git a/modules/private/mail/sieve_bin/imapsieve_copy b/modules/private/mail/sieve_bin/imapsieve_copy
new file mode 100755 (executable)
index 0000000..2ca1f23
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# Inspired from https://docs.iredmail.org/dovecot.imapsieve.html
+
+MSG_TYPE="$1"
+OUTPUT_DIR="/var/lib/vhost/.rspamd/${USER}/pending/${MSG_TYPE}"
+FILE="${OUTPUT_DIR}/$(date +%Y%m%d%H%M%S)-${RANDOM}${RANDOM}.eml"
+mkdir -p "${OUTPUT_DIR}"
+cat > ${FILE} < /dev/stdin
diff --git a/modules/private/mail/sieve_scripts/report_ham.sieve b/modules/private/mail/sieve_scripts/report_ham.sieve
new file mode 100644 (file)
index 0000000..f9b8481
--- /dev/null
@@ -0,0 +1,11 @@
+require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
+
+if environment :matches "imap.mailbox" "*" {
+    set "mailbox" "${1}";
+}
+
+if string "${mailbox}" "Trash" {
+    stop;
+}
+
+pipe :copy "imapsieve_copy" [ "ham" ];
diff --git a/modules/private/mail/sieve_scripts/report_spam.sieve b/modules/private/mail/sieve_scripts/report_spam.sieve
new file mode 100644 (file)
index 0000000..9a1f794
--- /dev/null
@@ -0,0 +1,3 @@
+require ["vnd.dovecot.pipe", "copy", "imapsieve" ];
+
+pipe :copy "imapsieve_copy" [ "spam" ];
index 6d87cdc9a05a48fc98002ae2cc38fef10129926f..8bb60d639db2ef916769bdfb37953ecb93ec0245 100644 (file)
@@ -17,7 +17,6 @@ rec {
     text = ''
       <?php
         $config['db_dsnw'] = '${env.psql_url}';
-        // This is used as default @domain, don't use "imap.immae.eu" here!
         $config['default_host'] = 'ssl://imap.immae.eu';
         $config['username_domain'] = array(
           "imap.immae.eu" => "mail.immae.eu"
@@ -46,6 +45,7 @@ rec {
           'identicon',
           'identity_select',
           'jqueryui',
+          'markasjunk',
           'managesieve',
           'newmail_notifier',
           'vcard_attachments',
@@ -60,11 +60,11 @@ rec {
 
         $config['language'] = 'fr_FR';
 
-        $config['drafts_mbox'] = 'Mail/Drafts';
-        $config['junk_mbox'] = 'Mail/Spam';
-        $config['sent_mbox'] = 'Mail/sent';
-        $config['trash_mbox'] = ''';
-        $config['default_folders'] = array('INBOX', 'Mail/Drafts', 'Mail/sent', 'Mail/Spam', ''');
+        $config['drafts_mbox'] = 'Drafts';
+        $config['junk_mbox'] = 'Junk';
+        $config['sent_mbox'] = 'Sent';
+        $config['trash_mbox'] = 'Trash';
+        $config['default_folders'] = array('INBOX', 'Drafts', 'Sent', 'Junk', 'Trash');
         $config['draft_autosave'] = 60;
         $config['enable_installer'] = false;
         $config['log_driver'] = 'file';
index 74f9d184b2703cee2d97f7f069dbe669ad567eee..ff9d477b10fdfd6349b48aabe14f73e061afdc0a 100644 (file)
@@ -50,4 +50,10 @@ rec {
     python = python3;
     inherit mylibs;
   };
+  dovecot_deleted-to-trash = callPackage ./dovecot/plugins/deleted_to_trash {
+    inherit mylibs;
+  };
+  dovecot_fts-xapian = callPackage ./dovecot/plugins/fts_xapian {
+    inherit mylibs;
+  };
 }
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/default.nix b/pkgs/dovecot/plugins/deleted_to_trash/default.nix
new file mode 100644 (file)
index 0000000..db1afb5
--- /dev/null
@@ -0,0 +1,21 @@
+{ stdenv, fetchurl, dovecot, mylibs, fetchpatch }:
+
+stdenv.mkDerivation (mylibs.fetchedGithub ./dovecot-deleted_to_trash.json // rec {
+  buildInputs = [ dovecot ];
+  patches = [
+    (fetchpatch {
+      name = "fix-dovecot-2.3.diff";
+      url = "https://github.com/lexbrugman/dovecot_deleted_to_trash/commit/c52a3799a96104a603ade33404ef6aa1db647b2f.diff";
+      sha256 = "0pld3rdcjp9df2qxbp807k6v4f48lyk0xy5q508ypa57d559y6dq";
+    })
+    ./fix_mbox.patch
+  ];
+  preConfigure = ''
+    substituteInPlace Makefile --replace \
+      "/usr/include/dovecot" \
+      "${dovecot}/include/dovecot"
+    substituteInPlace Makefile --replace \
+      "/usr/lib/dovecot/modules" \
+      "$out/lib/dovecot"
+    '';
+})
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json b/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json
new file mode 100644 (file)
index 0000000..2987a02
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "tag": "81b0754-master",
+  "meta": {
+    "name": "dovecot-deleted_to_trash",
+    "url": "https://github.com/lexbrugman/dovecot_deleted_to_trash",
+    "branch": "master"
+  },
+  "github": {
+    "owner": "lexbrugman",
+    "repo": "dovecot_deleted_to_trash",
+    "rev": "81b07549accfc36467bf8527a53c295c7a02dbb9",
+    "sha256": "1b3k31g898s4fa0a9l4kvjsdyds772waaay84sjdxv09jw6mqs0f",
+    "fetchSubmodules": true
+  }
+}
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch b/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch
new file mode 100644 (file)
index 0000000..0060fb4
--- /dev/null
@@ -0,0 +1,12 @@
+diff --git a/src/deleted-to-trash-plugin.c b/src/deleted-to-trash-plugin.c
+index bb4cc78..66bad53 100644
+--- a/src/deleted-to-trash-plugin.c
++++ b/src/deleted-to-trash-plugin.c
+@@ -82,6 +82,7 @@ static struct mailbox *mailbox_open_or_create(struct mailbox_list *list, const c
+       *error_r = mail_storage_get_last_error(mailbox_get_storage(box), &error);
+       if (error != MAIL_ERROR_NOTFOUND)
+       {
++              i_error("%s", *error_r);
+               mailbox_free(&box);
+               return NULL;
+       }
diff --git a/pkgs/dovecot/plugins/fts_xapian/default.nix b/pkgs/dovecot/plugins/fts_xapian/default.nix
new file mode 100644 (file)
index 0000000..350a3ff
--- /dev/null
@@ -0,0 +1,14 @@
+{ stdenv, autoconf, automake, pkg-config, dovecot, libtool, xapian, icu, mylibs }:
+
+stdenv.mkDerivation (mylibs.fetchedGithub ./fts-xapian.json // rec {
+  buildInputs = [ dovecot autoconf automake libtool pkg-config xapian icu ];
+  preConfigure = ''
+    export PANDOC=false
+    autoreconf -vi
+    '';
+  configureFlags = [
+    "--with-dovecot=${dovecot}/lib/dovecot"
+    "--without-dovecot-install-dirs"
+    "--with-moduledir=$(out)/lib/dovecot"
+  ];
+})
diff --git a/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json b/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json
new file mode 100644 (file)
index 0000000..a786776
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "tag": "9a94b4a-master",
+  "meta": {
+    "name": "fts-xapian",
+    "url": "https://github.com/grosjo/fts-xapian",
+    "branch": "master"
+  },
+  "github": {
+    "owner": "grosjo",
+    "repo": "fts-xapian",
+    "rev": "9a94b4aeaac3988786ad72a716127c306b05c9d6",
+    "sha256": "12xv5fnqahs0cy26ja2jwk6dg95626amblisf2wcx3nqzkcf4w1y",
+    "fetchSubmodules": true
+  }
+}