nixos/applications/email.nix

528 lines
15 KiB
Nix
Raw Permalink Normal View History

{
config,
pkgs,
lib,
inputs,
...
}:
with lib; let
name = "email";
cfg = config.services.skynet."${name}";
# 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);
2023-10-23 12:17:40 +00:00
# using +mailbox puts the mail in a seperate folder
create_skynet_email_int = accounts: mailbox: (map (account: "${account}@skynet.ie") accounts);
2023-10-23 12:17:40 +00:00
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";
2023-12-21 06:15:11 +00:00
members = ["admin" "trainee"];
}
];
sieveConfigFile =
# 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 (
2024-07-24 19:47:58 +00:00
address :localpart ["To", "Cc"] ["${toString create_config_to}"],
address :domain ["To", "Cc"] "skynet.ie"
){
2024-07-24 19:47:58 +00:00
if address :matches ["To", "Cc"] "*@skynet.ie" {
if header :is "X-Spam" "Yes" {
fileinto :create "''${1}.Junk";
stop;
} else {
fileinto :create "''${1}";
stop;
}
}
}
if allof (
address :localpart ["From"] ["${toString create_config_to}"],
address :domain ["From"] "skynet.ie"
){
if address :matches ["From"] "*@skynet.ie" {
if header :is "X-Spam" "Yes" {
fileinto :create "''${1}.Junk";
stop;
} else {
fileinto :create "''${1}";
stop;
}
}
}
'';
in {
imports = [
inputs.simple-nixos-mailserver.nixosModule
# for teh config
../config/users.nix
];
options.services.skynet."${name}" = {
# options that need to be passed in to make this work
enable = mkEnableOption "Skynet Email";
domain = mkOption {
type = types.str;
2023-07-04 20:53:24 +00:00
default = "skynet.ie";
description = lib.mdDoc "domaino";
};
sub = mkOption {
type = types.str;
default = "mail";
description = lib.mdDoc "mailserver subdomain";
};
2023-07-04 20:53:24 +00:00
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 = [
2023-07-15 14:05:57 +00:00
"/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 = {
"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
2024-07-17 03:20:48 +00:00
services.skynet.dns.records =
[
# core record
{
record = "@";
r_type = "MX";
# the number is the priority in teh case of multiple mailservers
value = "10 mail.${cfg.domain}.";
}
# basic one
{
record = "mail";
r_type = "A";
value = config.services.skynet.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 ""
# reverse pointer
{
record = config.services.skynet.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}.";
}
]
# SPF record
2024-07-17 03:20:48 +00:00
++ [
{
record = "${cfg.domain}.";
r_type = "TXT";
value = ''"v=spf1 a:${cfg.sub}.${cfg.domain} ip4:${config.services.skynet.host.ip} -all"'';
}
]
2023-07-16 11:32:18 +00:00
# DKIM keys
2024-07-17 03:20:48 +00:00
++ [
{
record = "mail._domainkey.skynet.ie.";
r_type = "TXT";
value = ''"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxju1Ie60BdHwyFVPNQKovL/cX9IFPzBKgjnHZf+WBzDCFKSBpf7NvnfXajtFDQN0poaN/Qfifid+V55ZCNDBn8Y3qZa4Y69iNiLw2DdvYf0HdnxX6+pLpbmj7tikGGLJ62xnhkJhoELnz5gCOhpyoiv0tSQVaJpaGZmoll861/QIDAQAB"'';
}
{
domain = "ulcompsoc.ie";
record = "mail._domainkey.ulcompsoc.ie.";
r_type = "TXT";
value = ''"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDl8ptSASx37t5sfmU2d2Y6yi9AVrsNFBZDmJ2uaLa4NuvAjxGQCw4wx+1Jui/HOuKYLpntLsjN851wgPR+3i51g4OblqBDvcHn9NYgWRZfHj9AASANQjdsaAbkXuyKuO46hZqeWlpESAcD6a4Evam4fkm+kiZC0+rccb4cWgsuLwIDAQAB"'';
}
]
# DMARC
2024-07-17 03:20:48 +00:00
++ [
{
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=quarantine"'';
}
];
2023-06-16 23:59:22 +00:00
#https://nixos-mailserver.readthedocs.io/en/latest/add-roundcube.html
users.groups.nginx = {};
2023-07-25 08:53:01 +00:00
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
2023-07-25 08:53:01 +00:00
$config['smtp_server'] = "ssl://${cfg.sub}.${cfg.domain}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
2023-07-25 18:29:51 +00:00
$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',
2023-07-25 18:29:51 +00:00
'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:*',
]
2023-07-25 18:29:51 +00:00
);
'';
};
# for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/275
services.dovecot2.sieve.extensions = ["fileinto"];
mailserver = {
enable = true;
fqdn = "${cfg.sub}.${cfg.domain}";
domains = [
cfg.domain
];
enableManageSieve = true;
2023-10-11 09:49:25 +00:00
lmtpSaveToDetailMailbox = "yes";
extraVirtualAliases = create_skynet_service_mailboxes;
2023-07-16 01:44:22 +00:00
# use the letsencrypt certs
certificateScheme = "acme";
2023-07-04 20:53:24 +00:00
# 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;
};
2023-07-04 20:53:24 +00:00
searchBase = cfg.ldap.searchBase;
searchScope = "sub";
dovecot = {
userFilter = "(skMail=%u)";
2023-07-16 14:06:06 +00:00
# can lock down how much space each user has access to from ldap
2023-07-16 14:18:23 +00:00
userAttrs = "quotaEmail=quota_rule=*:bytes=%$,=quota_rule2=Trash:storage=+100M";
2023-07-16 14:06:06 +00:00
2023-07-04 20:53:24 +00:00
# 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 = [
];
};
2024-02-09 12:47:20 +00:00
services.dovecot2.sieve.scripts = {
before = sieveConfigFile;
};
# This is to add a bcc to outgoing mail
# this then interacts with teh filters to put it in the right folder
# we can directly add to the postfix service here
services.postfix = let
# mostly copied from the upstream mailserver config/functions
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
sender_bcc_maps_file = let
content = lookupTableToString create_skynet_service_bcc;
in
builtins.toFile "sender_bcc_maps" content;
lookupTableToString = attrs: let
valueToString = value: lib.concatStringsSep ", " value;
in
lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
# convert the mailboxes config to something that can be used here
create_skynet_email_bcc = mailbox: {
name = "${mailbox}@skynet.ie";
value = ["${mailbox}@skynet.ie"];
};
create_skynet_service_bcc = builtins.listToAttrs (map (mailbox: (create_skynet_email_bcc mailbox.account)) service_mailboxes);
in {
mapFiles."sender_bcc_maps" = sender_bcc_maps_file;
config = {
sender_bcc_maps = [
(mappedFile "sender_bcc_maps")
];
};
};
# 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
}
'';
*/
};
}