From a929614f94d11a4f397e72e74f38b3212c24cdee Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Sun, 23 Jun 2019 21:04:55 +0200 Subject: [PATCH] Configure mail (dovecot, postfix, spam checks) --- modules/myids.nix | 6 +- modules/private/default.nix | 7 +- modules/private/dns.nix | 2 +- modules/private/mail/default.nix | 12 + modules/private/mail/dovecot.nix | 255 ++++++++++++++++++ .../private/{mail.nix => mail/milters.nix} | 26 +- modules/private/mail/postfix.nix | 227 ++++++++++++++++ modules/private/mail/rspamd.nix | 84 ++++++ modules/private/mail/scan_reported_mails | 21 ++ modules/private/mail/sieve_bin/imapsieve_copy | 8 + .../mail/sieve_scripts/report_ham.sieve | 11 + .../mail/sieve_scripts/report_spam.sieve | 3 + .../websites/tools/tools/roundcubemail.nix | 12 +- pkgs/default.nix | 6 + .../plugins/deleted_to_trash/default.nix | 21 ++ .../dovecot-deleted_to_trash.json | 15 ++ .../plugins/deleted_to_trash/fix_mbox.patch | 12 + pkgs/dovecot/plugins/fts_xapian/default.nix | 14 + .../plugins/fts_xapian/fts-xapian.json | 15 ++ 19 files changed, 737 insertions(+), 20 deletions(-) create mode 100644 modules/private/mail/default.nix create mode 100644 modules/private/mail/dovecot.nix rename modules/private/{mail.nix => mail/milters.nix} (83%) create mode 100644 modules/private/mail/postfix.nix create mode 100644 modules/private/mail/rspamd.nix create mode 100755 modules/private/mail/scan_reported_mails create mode 100755 modules/private/mail/sieve_bin/imapsieve_copy create mode 100644 modules/private/mail/sieve_scripts/report_ham.sieve create mode 100644 modules/private/mail/sieve_scripts/report_spam.sieve create mode 100644 pkgs/dovecot/plugins/deleted_to_trash/default.nix create mode 100644 pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json create mode 100644 pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch create mode 100644 pkgs/dovecot/plugins/fts_xapian/default.nix create mode 100644 pkgs/dovecot/plugins/fts_xapian/fts-xapian.json diff --git a/modules/myids.nix b/modules/myids.nix index 7ec9c0e..e949ca7 100644 --- a/modules/myids.nix +++ b/modules/myids.nix @@ -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; diff --git a/modules/private/default.nix b/modules/private/default.nix index 894efb7..026e69d 100644 --- a/modules/private/default.nix +++ b/modules/private/default.nix @@ -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; diff --git a/modules/private/dns.nix b/modules/private/dns.nix index f12f982..6647c14 100644 --- a/modules/private/dns.nix +++ b/modules/private/dns.nix @@ -106,7 +106,7 @@ '' ; ------------------ 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 index 0000000..ad2c684 --- /dev/null +++ b/modules/private/mail/default.nix @@ -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 index 0000000..d757f59 --- /dev/null +++ b/modules/private/mail/dovecot.nix @@ -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; + }; + }; +} + diff --git a/modules/private/mail.nix b/modules/private/mail/milters.nix similarity index 83% rename from modules/private/mail.nix rename to modules/private/mail/milters.nix index eb869ba..c4bd990 100644 --- a/modules/private/mail.nix +++ b/modules/private/mail/milters.nix @@ -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 index 0000000..53bf650 --- /dev/null +++ b/modules/private/mail/postfix.nix @@ -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 index 0000000..3a7a67c --- /dev/null +++ b/modules/private/mail/rspamd.nix @@ -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 index 0000000..fe9f4d6 --- /dev/null +++ b/modules/private/mail/scan_reported_mails @@ -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 index 0000000..2ca1f23 --- /dev/null +++ b/modules/private/mail/sieve_bin/imapsieve_copy @@ -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 index 0000000..f9b8481 --- /dev/null +++ b/modules/private/mail/sieve_scripts/report_ham.sieve @@ -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 index 0000000..9a1f794 --- /dev/null +++ b/modules/private/mail/sieve_scripts/report_spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve" ]; + +pipe :copy "imapsieve_copy" [ "spam" ]; diff --git a/modules/private/websites/tools/tools/roundcubemail.nix b/modules/private/websites/tools/tools/roundcubemail.nix index 6d87cdc..8bb60d6 100644 --- a/modules/private/websites/tools/tools/roundcubemail.nix +++ b/modules/private/websites/tools/tools/roundcubemail.nix @@ -17,7 +17,6 @@ rec { text = '' "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'; diff --git a/pkgs/default.nix b/pkgs/default.nix index 74f9d18..ff9d477 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -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 index 0000000..db1afb5 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/default.nix @@ -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 index 0000000..2987a02 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json @@ -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 index 0000000..0060fb4 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch @@ -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 index 0000000..350a3ff --- /dev/null +++ b/pkgs/dovecot/plugins/fts_xapian/default.nix @@ -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 index 0000000..a786776 --- /dev/null +++ b/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json @@ -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 + } +} -- 2.41.0