{ 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); create_skynet_email = accounts: mailbox: (map (account: "${account}+${mailbox}@skynet.ie") accounts); create_skynet_email_admin = mailbox: (create_skynet_email config.skynet.users.admin mailbox) ++ ["${mailbox}_int@skynet.ie"]; create_skynet_email_committee = mailbox: (create_skynet_email config.skynet.users.committee mailbox) ++ ["${mailbox}_int@skynet.ie"]; 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; skynet_acme.domains = [ "${cfg.sub}.${cfg.domain}" ]; # set up dns record for it skynet_dns.records = [ # basic one { record = "mail"; r_type = "A"; value = cfg.host.ip; } # 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"; value = ''"v=DMARC1; p=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}."; } ]; # to provide the certs services.nginx.virtualHosts = { "${cfg.sub}.${cfg.domain}" = { forceSSL = true; useACMEHost = "skynet"; # override the inbuilt nginx config enableACME = false; serverName = "${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 ]; lmtpSaveToDetailMailbox = "yes"; extraVirtualAliases = { "abuse@skynet.ie" = create_skynet_email_admin "abuse"; "accounts@skynet.ie" = create_skynet_email_committee "accounts"; "compsoc@skynet.ie" = create_skynet_email_committee "compsoc"; "contact@skynet.ie" = create_skynet_email_committee "contact"; "dbadmin@skynet.ie" = create_skynet_email_admin "dbadmin"; "dnsadm@skynet.ie" = create_skynet_email_admin "dnsadm"; "hostmaster@skynet.ie" = create_skynet_email_admin "hostmaster"; "intersocsrep@skynet.ie" = create_skynet_email_committee "intersocsrep"; "mailman@skynet.ie" = create_skynet_email_admin "mailman"; "security@skynet.ie" = create_skynet_email_admin "security"; "sysadm@skynet.ie" = create_skynet_email_admin "sysadm"; "webadmin@skynet.ie" = create_skynet_email_admin "webadmin"; "pycon2023@skynet.ie" = create_skynet_email_committee "pycon2023"; }; # 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 = [ ]; }; # 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 } ''; */ }; }