{ config, pkgs, lib, inputs, ... }: with lib; let cfg = config.services.skynet_email; # create teh new strings create_filter_array = map (x: "(memberOf=cn=${x},ou=groups,${cfg.ldap.base})"); create_filter_join = x: concatStringsSep "" x; # thought you could escape racket? create_filter = groups: create_filter_join (create_filter_array groups); # using +mailbox puts the mail in a seperate folder create_skynet_email_int = accounts: mailbox: (map (account: "${account}@skynet.ie") accounts); groups_to_accounts = groups: builtins.concatMap (x: config.skynet.users.${x}) groups; create_skynet_email_attribute = mailbox: groups: (create_skynet_email_int (groups_to_accounts groups) mailbox) ++ ["int_${mailbox}@skynet.ie"]; create_skynet_email = mailbox: groups: { name = "${mailbox}@skynet.ie"; value = create_skynet_email_attribute mailbox groups; }; create_skynet_service_mailboxes = builtins.listToAttrs (map (mailbox: (create_skynet_email mailbox.account mailbox.members)) service_mailboxes); create_config_to = concatStringsSep "\",\"" (map (mailbox: "${mailbox.account}") service_mailboxes); service_mailboxes = [ { account = "root"; members = ["admin"]; } { account = "abuse"; members = ["admin"]; } { account = "accounts"; members = ["committee"]; } { account = "compsoc"; members = ["committee"]; } { account = "contact"; members = ["committee"]; } { account = "dbadmin"; members = ["admin"]; } { account = "dnsadm"; members = ["admin"]; } { account = "hostmaster"; members = ["admin"]; } { account = "intersocsrep"; members = ["committee"]; } { account = "mailman"; members = ["admin"]; } { account = "security"; members = ["admin"]; } { account = "sysadm"; members = ["admin"]; } { account = "webadmin"; members = ["admin"]; } { account = "pycon2023"; members = ["committee"]; } { account = "skynet_topdesk"; members = ["admin" "trainee"]; } ]; configFile = # https://doc.dovecot.org/configuration_manual/sieve/examples/#plus-addressed-mail-filtering pkgs.writeText "basic_sieve" '' require "copy"; require "mailbox"; require "imap4flags"; require ["fileinto", "reject"]; require "variables"; require "regex"; # this should be close to teh last step if allof ( address :localpart ["To"] ["${toString create_config_to}"], address :domain ["To"] "skynet.ie" ){ if address :matches ["To"] "*@skynet.ie" { if header :is "X-Spam" "Yes" { fileinto :create "''${1}.Junk"; stop; } else { fileinto :create "''${1}"; } } } ''; in { imports = [ ./dns.nix ./acme.nix ./nginx.nix inputs.simple-nixos-mailserver.nixosModule # for teh config ../config/users.nix ]; options.services.skynet_email = { # options that need to be passed in to make this work enable = mkEnableOption "Skynet Email"; host = { ip = mkOption { type = types.str; }; name = mkOption { type = types.str; }; }; domain = mkOption { type = types.str; default = "skynet.ie"; description = lib.mdDoc "domaino"; }; sub = mkOption { type = types.str; default = "mail"; description = lib.mdDoc "mailserver subdomain"; }; groups = mkOption { type = types.listOf types.str; default = [ # general skynet users "skynet-users" # C&S folsk get access "skynet-cns" # skynet service accounts "skynet-service" ]; description = lib.mdDoc "Groups we want to allow access to the email"; }; ldap = { hosts = mkOption { type = types.listOf types.str; default = [ "ldaps://account.skynet.ie" ]; description = lib.mdDoc "ldap domains"; }; base = mkOption { type = types.str; default = "dc=skynet,dc=ie"; description = lib.mdDoc "where to find users"; }; searchBase = mkOption { type = types.str; default = "ou=users,${cfg.ldap.base}"; description = lib.mdDoc "where to find users"; }; bind_dn = mkOption { type = types.str; default = "cn=admin,${cfg.ldap.base}"; description = lib.mdDoc "where to find users"; }; }; }; config = mkIf cfg.enable { services.skynet_backup.normal.backups = [ "/var/vmail" "/var/dkim" ]; age.secrets.ldap_pw.file = ../secrets/ldap/pw.age; security.acme.certs = { "mail" = { domain = "mail.skynet.ie"; extraDomainNames = [ "imap.skynet.ie" "pop3.skynet.ie" "smtp.skynet.ie" ]; }; "imap" = { domain = "imap.skynet.ie"; extraDomainNames = [ "mail.skynet.ie" "pop3.skynet.ie" "smtp.skynet.ie" ]; }; "pop3" = { domain = "pop3.skynet.ie"; extraDomainNames = [ "imap.skynet.ie" "mail.skynet.ie" "smtp.skynet.ie" ]; }; "smtp" = { domain = "smtp.skynet.ie"; extraDomainNames = [ "imap.skynet.ie" "pop3.skynet.ie" "mail.skynet.ie" ]; }; }; # to provide the certs services.nginx.virtualHosts = { "${cfg.host.ip}" = { forceSSL = true; useACMEHost = "skynet"; locations."/".return = "307 https://skynet.ie"; }; "mail.skynet.ie" = { forceSSL = true; useACMEHost = "mail"; # override the inbuilt nginx config enableACME = false; serverName = "mail.skynet.ie"; }; "imap.skynet.ie" = { forceSSL = true; useACMEHost = "imap"; # override the inbuilt nginx config enableACME = false; serverName = "imap.skynet.ie"; }; "pop3.skynet.ie" = { forceSSL = true; useACMEHost = "pop3"; # override the inbuilt nginx config enableACME = false; serverName = "pop3.skynet.ie"; }; "smtp.skynet.ie" = { forceSSL = true; useACMEHost = "smtp"; # override the inbuilt nginx config enableACME = false; serverName = "smtp.skynet.ie"; }; }; # set up dns record for it skynet_dns.records = [ # basic one { record = "mail"; r_type = "A"; value = cfg.host.ip; } #DNS config for K-9 Mail { record = "imap"; r_type = "CNAME"; value = "mail"; } { record = "pop3"; r_type = "CNAME"; value = "mail"; } { record = "smtp"; r_type = "CNAME"; value = "mail"; } # TXT records, all tehse are inside escaped strings to allow using "" # SPF record { record = "${cfg.domain}."; r_type = "TXT"; value = ''"v=spf1 a:${cfg.sub}.${cfg.domain} -all"''; } # DKIM keys { record = "mail._domainkey.skynet.ie."; r_type = "TXT"; value = ''"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxju1Ie60BdHwyFVPNQKovL/cX9IFPzBKgjnHZf+WBzDCFKSBpf7NvnfXajtFDQN0poaN/Qfifid+V55ZCNDBn8Y3qZa4Y69iNiLw2DdvYf0HdnxX6+pLpbmj7tikGGLJ62xnhkJhoELnz5gCOhpyoiv0tSQVaJpaGZmoll861/QIDAQAB"''; } { record = "mail._domainkey.ulcompsoc.ie."; r_type = "TXT"; value = ''"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDl8ptSASx37t5sfmU2d2Y6yi9AVrsNFBZDmJ2uaLa4NuvAjxGQCw4wx+1Jui/HOuKYLpntLsjN851wgPR+3i51g4OblqBDvcHn9NYgWRZfHj9AASANQjdsaAbkXuyKuO46hZqeWlpESAcD6a4Evam4fkm+kiZC0+rccb4cWgsuLwIDAQAB"''; } # DMARC { record = "_dmarc.${cfg.domain}."; r_type = "TXT"; # p : quarantine => sends to spam, reject => never sent # rua : mail that receives reports about DMARC activity # pct : percentage of unathenticated messages that DMARC stops # adkim : alignment policy for DKIM, s => Strict, subdomains arent allowed, r => relaxed, subdomains allowed # aspf : alignment policy for SPF, s => Strict, subdomains arent allowed, r => relaxed, subdomains allowed # sp : DMARC policy for subdomains, none => no action, reports to rua, quarantine => spam, reject => never sent value = ''"v=DMARC1; p=quarantine; rua=mailto:mailman@skynet.ie; pct=100; adkim=s; aspf=s; sp=none"''; } # reverse pointer { record = cfg.host.ip; r_type = "PTR"; value = "${cfg.sub}.${cfg.domain}."; } # SRV records to help gmail on android etc find the correct mail.skynet.ie domain for config rather than just defaulting to skynet.ie # https://serverfault.com/questions/935192/how-to-setup-auto-configure-email-for-android-mail-app-on-your-server/1018406#1018406 # response should be: # _imap._tcp SRV 0 1 143 imap.example.com. { record = "_imaps._tcp"; r_type = "SRV"; value = "0 1 993 ${cfg.sub}.${cfg.domain}."; } { record = "_imap._tcp"; r_type = "SRV"; value = "0 1 143 ${cfg.sub}.${cfg.domain}."; } { record = "_submissions._tcp"; r_type = "SRV"; value = "0 1 465 ${cfg.sub}.${cfg.domain}."; } { record = "_submission._tcp"; r_type = "SRV"; value = "0 1 587 ${cfg.sub}.${cfg.domain}."; } ]; #https://nixos-mailserver.readthedocs.io/en/latest/add-roundcube.html users.groups.nginx = {}; users.groups.roundcube = {}; services.roundcube = { enable = true; # this is the url of the vhost, not necessarily the same as the fqdn of # the mailserver hostName = "${cfg.sub}.${cfg.domain}"; extraConfig = '' # starttls needed for authentication, so the fqdn required to match # the certificate $config['smtp_server'] = "ssl://${cfg.sub}.${cfg.domain}"; $config['smtp_user'] = "%u"; $config['smtp_pass'] = "%p"; $config['imap_host'] = "ssl://${cfg.sub}.${cfg.domain}"; $config['product_name'] = "Skynet Webmail"; $config['identities_level'] = 4; $config['login_username_filter'] = "email"; $config['ldap_public']['public'] = array( 'name' => 'Public LDAP Addressbook', 'hosts' => 'tls://account.skynet.ie', 'port' => 636 , 'user_specific' => false, 'base_dn' => 'ou=users,dc=skynet,dc=ie', 'filter' => '(skMemberOf=cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie)', 'fieldmap' => [ // Roundcube => LDAP:limit 'name' => 'cn', 'surname' => 'sn', 'email' => 'skMail:*', ] ); ''; }; mailserver = { enable = true; fqdn = "${cfg.sub}.${cfg.domain}"; domains = [ cfg.domain ]; enableManageSieve = true; lmtpSaveToDetailMailbox = "yes"; extraVirtualAliases = create_skynet_service_mailboxes; # use the letsencrypt certs certificateScheme = "acme"; # 20MB max size messageSizeLimit = 20000000; ldap = { enable = true; uris = cfg.ldap.hosts; bind = { dn = cfg.ldap.bind_dn; passwordFile = config.age.secrets.ldap_pw.path; }; searchBase = cfg.ldap.searchBase; searchScope = "sub"; dovecot = { userFilter = "(skMail=%u)"; # can lock down how much space each user has access to from ldap userAttrs = "quotaEmail=quota_rule=*:bytes=%$,=quota_rule2=Trash:storage=+100M"; # accept emails in, but only allow access to paid up members passFilter = "(&(|${create_filter cfg.groups})(skMail=%u))"; }; postfix = { filter = "(|(skMail=%s)(uid=%s))"; uidAttribute = "skMail"; mailAttribute = "skMail"; }; }; # feckin spammers rejectRecipients = [ ]; }; services.dovecot2.sieveScripts = { before = configFile; }; # tune the spam filter /* services.rspamd.extraConfig = '' actions { reject = null; # Disable rejects, default is 15 add_header = 7; # Add header when reaching this score greylist = 4; # Apply greylisting when reaching this score } ''; */ }; }