machines/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix

268 lines
8.1 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.khscodes."mx.kaareskovgaard.net";
passDbFile = "/run/secret/dovecot/passwd";
# This just replicates what simple-nixos-mailserver does, but using the proper usernames
userDbFile = pkgs.writeTextFile {
name = "userdb";
text = lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: value:
"${name}:::::::"
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.accounts
);
};
bogusPasswdFile = pkgs.writeTextFile {
name = "bogus-passwd";
text = "$6$1234";
};
accountPrimaryEmail =
name: account: if account.isLdapAccount then lib.lists.head account.aliases else name;
accountAlternativeEmails =
name: account:
if account.isLdapAccount then
lib.lists.ifilter0 (idx: _: idx > 0) account.aliases
else
account.aliases;
accountOption = lib.khscodes.mkSubmodule {
description = "mail account";
options = {
name = lib.mkOption {
type = lib.types.str;
example = "user1@example.com";
description = "Username";
};
aliases = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [
"abuse@example.com"
"postmaster@example.com"
];
default = [ ];
description = ''
A list of aliases of this login account.
Note: Use list entries like "@example.com" to create a catchAll
that allows sending from all email addresses in these domain.
'';
};
aliasesRegexp = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ ''/^tom\..*@domain\.com$/'' ];
default = [ ];
description = ''
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
'';
};
catchAll = lib.mkOption {
type = lib.types.listOf (lib.types.enum config.mailserver.domains);
example = [
"example.com"
"example2.com"
];
default = [ ];
description = ''
For which domains should this account act as a catch all?
Note: Does not allow sending from all addresses of these domains.
'';
};
quota = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "2G";
description = ''
Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
obvious meaning. Leave blank for the standard quota `100G`.
'';
};
sieveScript = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
example = ''
require ["fileinto", "mailbox"];
if address :is "from" "gitlab@mg.gitlab.com" {
fileinto :create "GitLab";
stop;
}
# This must be the last rule, it will check if list-id is set, and
# file the message into the Lists folder for further investigation
elsif header :matches "list-id" "<?*>" {
fileinto :create "Lists";
stop;
}
'';
description = ''
Per-user sieve script.
'';
};
sendOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Specifies if the account should be a send-only account.
Emails sent to send-only accounts will be rejected from
unauthorized senders with the `sendOnlyRejectMessage`
stating the reason.
'';
};
sendOnlyRejectMessage = lib.mkOption {
type = lib.types.str;
default = "This account cannot receive emails.";
description = ''
The message that will be returned to the sender when an email is
sent to a send-only account. Only used if the account is marked
as send-only.
'';
};
isLdapAccount = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Marks the account as an LDAP account. Non LDAP accounts gets passwords provisioned, such that other services can use them";
};
};
};
systemAccounts = lib.attrsets.foldlAttrs (
acc: name: value:
if value.isLdapAccount then acc else acc ++ [ (accountPrimaryEmail name value) ]
) [ ] cfg.accounts;
systemAccountsPassDbTemplateContents =
lib.concatStringsSep "\n" (
lib.lists.map (account: ''
{{- with secret "mx.kaareskovgaard.net/data/users/${account}" -}}
"${account}:{{ .Data.data.hashed_password }}::::::"
{{- end -}}
'') systemAccounts
)
# Just make sure the file is not empty
+ "\n";
data = import ./accounts/mailbox_map.nix {
inherit lib accountPrimaryEmail accountAlternativeEmails;
accounts = cfg.accounts;
extraVirtualAliases = { };
};
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
in
{
options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption {
type = lib.types.attrsOf accountOption;
default = { };
};
config = {
mailserver = {
loginAccounts = lib.attrsets.mapAttrs' (name: value: {
name = accountPrimaryEmail name value;
value = {
inherit (value)
name
aliasesRegexp
catchAll
quota
sieveScript
sendOnly
sendOnlyRejectMessage
;
aliases = accountAlternativeEmails name value;
hashedPasswordFile = bogusPasswdFile;
};
}) cfg.accounts;
extraVirtualAliases = data.mailserverExtraVirtualAliases;
};
services.postfix = {
mapFiles."valias_maps" = data.valiases_file;
mapFiles."regex_valias_maps" = data.regex_valiases_file;
mapFiles."vaccounts" = lib.mkForce data.vaccounts_file;
mapFiles."regex_vaccounts" = lib.mkForce data.regex_vaccounts_file;
config.virtual_mailbox_maps = lib.mkForce [
(mappedFile "valias_maps")
(mappedRegexFile "regex_valias_maps")
];
};
khscodes.infrastructure.vault-server-approle.policy = {
"mx.kaareskovgaard.net/data/users/*" = {
capabilities = [ "read" ];
};
};
khscodes.infrastructure.provisioning.pre.modules = [
{
terraform.required_providers.random = {
source = "hashicorp/random";
version = "3.7.2";
};
provider.random = { };
}
]
++ (lib.lists.map (
account:
let
tfName = lib.khscodes.sanitize-terraform-name account;
in
{
resource.random_password."${tfName}" = {
length = 48;
numeric = true;
lower = true;
upper = true;
special = false;
};
resource.vault_kv_secret_v2."${tfName}" = {
mount = config.khscodes.vault.output.mount."mx.kaareskovgaard.net".path;
name = "users/${account}";
data_json = ''
{
"hashed_password": ''${ jsonencode(resource.random_password.${tfName}.bcrypt_hash) },
"password": ''${ jsonencode(resource.random_password.${tfName}.result) }
}
'';
};
}
) systemAccounts);
khscodes.services.vault-agent.templates = [
{
contents = systemAccountsPassDbTemplateContents;
destination = passDbFile;
perms = "0600";
owner = "dovecot2";
group = "dovecot2";
restartUnits = [
"dovecot2.service"
];
}
];
systemd.services.dovecot2 = {
unitConfig.ConditionPathExists = [ passDbFile ];
serviceConfig.ReadOnlyPaths = [ passDbFile ];
# simple-nixos-mailserver creates its own passwd file,
# but the passwords in that file are all bogus, so replace them
# with our own.
preStart = lib.mkAfter ''
cp ${passDbFile} /run/dovecot2/passwd
cp ${userDbFile} /run/dovecot2/userdb
'';
};
# This prevents local usernames without domain names to get rewritten.
# services.postfix.submissionOptions = submissionOptions;
# services.postfix.submissionsOptions = submissionOptions;
};
}