I think I finally understand how the postfix and dovecot
integration works now. Now the regular accounts should work again, and with proper handling of catch all aliases for domains, as well as handling postmaster and abuse emails being forwarded to khs with proper auto tagging of the mails.
This commit is contained in:
parent
9c4a751fe0
commit
6ac55b7e44
9 changed files with 95 additions and 189 deletions
|
@ -35,9 +35,9 @@ let
|
|||
runtimeInputs = [ pkgs.systemd ];
|
||||
text = ''
|
||||
chown ${lib.escapeShellArg template.owner}:${lib.escapeShellArg template.group} ${lib.escapeShellArg template.destination}
|
||||
${template.exec}
|
||||
${restartUnits template.restartUnits}
|
||||
${reloadOrRestartUnits template.reloadOrRestartUnits}
|
||||
${template.exec}
|
||||
'';
|
||||
meta = {
|
||||
mainProgram = "restart-command";
|
||||
|
|
|
@ -34,9 +34,13 @@
|
|||
];
|
||||
};
|
||||
"mx.kaareskovgaard.net" = {
|
||||
postmaster = "kaare+postmaster@agerlin-skovgaard.dk";
|
||||
abuse = "kaare+abuse@agerlin-skovgaard.dk";
|
||||
domains = [
|
||||
"agerlin-skovgaard.dk"
|
||||
"agerlinskovgaard.dk"
|
||||
"k.agerlin-skovgaard.dk"
|
||||
"k.agerlinskovgaard.dk"
|
||||
];
|
||||
accounts = import ./users.nix;
|
||||
};
|
||||
|
|
|
@ -7,22 +7,39 @@
|
|||
let
|
||||
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||
passDbFile = "/run/secret/dovecot/passwd";
|
||||
isCatchAllAddress = addr: lib.strings.hasPrefix "@" addr;
|
||||
bogusPasswdFile = pkgs.writeTextFile {
|
||||
name = "bogus-passwd";
|
||||
text = "$6$1234";
|
||||
};
|
||||
userDbAccountOpts =
|
||||
name: account:
|
||||
lib.strings.concatStringsSep " " (
|
||||
(lib.lists.optional (account.quota != null) "userdb_quota_rule=*:storage=${account.quota}")
|
||||
++ [ "userdb_user=${name}" ]
|
||||
);
|
||||
userDbLine =
|
||||
addr: name: account:
|
||||
"${addr}:::::::${userDbAccountOpts name account}";
|
||||
userDbFile = pkgs.writeTextFile {
|
||||
name = "userdb";
|
||||
text = '''';
|
||||
text = lib.strings.concatStringsSep "\n" (
|
||||
lib.lists.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: account:
|
||||
[
|
||||
(userDbLine name name account)
|
||||
]
|
||||
++ (lib.lists.map (addr: userDbLine addr name account) (
|
||||
# Don't include catch all addresses, they become aliases in postfix
|
||||
lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses
|
||||
))
|
||||
) cfg.accounts
|
||||
)
|
||||
);
|
||||
};
|
||||
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;
|
||||
firstNonCatchAllAddress =
|
||||
account: lib.lists.head (lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses);
|
||||
accountOption = lib.khscodes.mkSubmodule {
|
||||
description = "mail account";
|
||||
options = {
|
||||
|
@ -32,7 +49,7 @@ let
|
|||
description = "Username";
|
||||
};
|
||||
|
||||
aliases = lib.mkOption {
|
||||
addresses = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
example = [
|
||||
"abuse@example.com"
|
||||
|
@ -40,31 +57,9 @@ let
|
|||
];
|
||||
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.
|
||||
A list of addresses for the account.
|
||||
Note: Use list entires like "@example.com" to create a catchAll
|
||||
that allows sending from an email address in these domains.
|
||||
'';
|
||||
};
|
||||
|
||||
|
@ -145,6 +140,31 @@ let
|
|||
)
|
||||
# Just make sure the file is not empty
|
||||
+ "\n";
|
||||
|
||||
catchAllAliases = lib.strings.concatStringsSep "\n" (
|
||||
lib.lists.flatten (
|
||||
lib.attrsets.mapAttrsToList (
|
||||
name: account:
|
||||
let
|
||||
regularEmailAddr = firstNonCatchAllAddress account;
|
||||
catchAlls = lib.lists.filter isCatchAllAddress account.addresses;
|
||||
in
|
||||
lib.lists.map (addr: "${addr} ${regularEmailAddr}") catchAlls
|
||||
) cfg.accounts
|
||||
)
|
||||
);
|
||||
postmasterAliases = lib.strings.concatStringsSep "\n" (
|
||||
lib.lists.map (domain: "postmaster@${domain} ${cfg.postmaster}") cfg.domains
|
||||
);
|
||||
|
||||
abuseAliases = lib.strings.concatStringsSep "\n" (
|
||||
lib.lists.map (domain: "abuse@${domain} ${cfg.abuse}") cfg.domains
|
||||
);
|
||||
extraVirtualAliases = lib.strings.concatStringsSep "\n" [
|
||||
catchAllAliases
|
||||
postmasterAliases
|
||||
abuseAliases
|
||||
];
|
||||
in
|
||||
{
|
||||
options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption {
|
||||
|
@ -156,14 +176,12 @@ in
|
|||
loginAccounts = lib.attrsets.mapAttrs (name: value: {
|
||||
inherit (value)
|
||||
name
|
||||
aliasesRegexp
|
||||
catchAll
|
||||
quota
|
||||
sieveScript
|
||||
sendOnly
|
||||
sendOnlyRejectMessage
|
||||
aliases
|
||||
;
|
||||
aliases = value.addresses;
|
||||
hashedPasswordFile = bogusPasswdFile;
|
||||
}) cfg.accounts;
|
||||
extraVirtualAliases = { };
|
||||
|
@ -228,9 +246,18 @@ in
|
|||
# with our own.
|
||||
preStart = lib.mkAfter ''
|
||||
cp ${passDbFile} /run/dovecot2/passwd
|
||||
# cp ${userDbFile} /run/dovecot2/userdb
|
||||
cp ${userDbFile} /run/dovecot2/userdb
|
||||
'';
|
||||
};
|
||||
# The stuff simple nixos-mailserver sets up here creates aliases to the accounts listed,
|
||||
# but I use usernames for the accounts, which ends up creating forwards to stuff like khs@mx.kaareskovgaard.net, which causes issues.
|
||||
# Instead I just want the entries in virtual_mailbox_maps for all regular addresses, which will leave the addresses alone (ie. not rewrite them)
|
||||
# and send them off via lmtp to dovecot2. Dovecot2 has a fixed userdb which accepts all regular email addresses for every login and maps it to that
|
||||
# users mailbox. The only thing not handled this way is catchAll addresses (and regexes, which I do not support at the moment, and hopefully won't need).
|
||||
services.postfix = {
|
||||
virtual = lib.mkForce extraVirtualAliases;
|
||||
config.virtual_alias_maps = lib.mkForce [ "hash:/etc/postfix/virtual" ];
|
||||
};
|
||||
# This prevents local usernames without domain names to get rewritten.
|
||||
# services.postfix.submissionOptions = submissionOptions;
|
||||
# services.postfix.submissionsOptions = submissionOptions;
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
accounts,
|
||||
accountPrimaryEmail,
|
||||
accountAlternativeEmails,
|
||||
extraVirtualAliases ? { },
|
||||
}:
|
||||
let
|
||||
# I would like to not use emails as usernames, but this is not something postfix is particularly happy with,
|
||||
# as far as I can see. I *think* I can do this by randomly selecting an address, ie. the first, as the "primary"
|
||||
# address, and then map all other addresses to that one. Then create a virtual mailbox map that maps that back
|
||||
# to the username before delivering it to lmtp. All of this just worked when using ldap, but that meant
|
||||
# not using code configured accounts set up with opentofu as well, and would also make the email delivery
|
||||
# take a hard dependency on ldap (and not just the login process).
|
||||
|
||||
# Most of these functions are almost the same as what is found inside nixos-simple-mailserver,
|
||||
# just modified with this in mind.
|
||||
|
||||
# Merge several lookup tables. A lookup table is a attribute set where
|
||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
||||
# - the value is a list of addresses
|
||||
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
|
||||
|
||||
# lookupTableToString :: Map String [String] -> String
|
||||
lookupTableToString =
|
||||
attrs:
|
||||
let
|
||||
valueToString = value: lib.concatStringsSep ", " value;
|
||||
in
|
||||
lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs
|
||||
);
|
||||
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
||||
attrsToLookupTable =
|
||||
aliases:
|
||||
let
|
||||
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
||||
in
|
||||
mergeLookupTables lookupTables;
|
||||
|
||||
# valiases_postfix :: Map String [String]
|
||||
valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) (
|
||||
(accountAlternativeEmails name value) ++ lib.singleton (accountPrimaryEmail name value)
|
||||
)
|
||||
) accounts
|
||||
)
|
||||
);
|
||||
# catchAllPostfix :: Map String [String]
|
||||
catchAllPostfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "@${from}" = to; }) value.catchAll
|
||||
) accounts
|
||||
)
|
||||
);
|
||||
|
||||
# mailserverExtraVirtualAliases :: Map String (Map String)
|
||||
mailserverExtraVirtualAliases = lib.attrsets.mapAttrs (
|
||||
name: value:
|
||||
let
|
||||
value = if lib.lists.isList value then value else [ value ];
|
||||
to = name;
|
||||
in
|
||||
{
|
||||
name = to;
|
||||
value = lib.lists.map (
|
||||
addr:
|
||||
assert (lib.attrsets.hasAttr addr accounts);
|
||||
accountPrimaryEmail name accounts."${addr}"
|
||||
) value;
|
||||
}
|
||||
) extraVirtualAliases;
|
||||
|
||||
# extra_valiases_postfix :: Map String [String]
|
||||
extra_valiases_postfix = attrsToLookupTable extraVirtualAliases;
|
||||
|
||||
# all_valiases_postfix :: Map String [String]
|
||||
all_valiases_postfix = mergeLookupTables [
|
||||
valiases_postfix
|
||||
extra_valiases_postfix
|
||||
];
|
||||
|
||||
# valiases_file :: Path
|
||||
valiases_file =
|
||||
let
|
||||
content = lookupTableToString (mergeLookupTables [
|
||||
all_valiases_postfix
|
||||
catchAllPostfix
|
||||
]);
|
||||
in
|
||||
builtins.toFile "valias" content;
|
||||
|
||||
regex_valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) value.aliasesRegexp
|
||||
) accounts
|
||||
)
|
||||
);
|
||||
regex_valiases_file =
|
||||
let
|
||||
content = lookupTableToString regex_valiases_postfix;
|
||||
in
|
||||
builtins.toFile "regex_valias" content;
|
||||
|
||||
# vaccounts_file :: Path
|
||||
# see
|
||||
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
|
||||
# for details on how this file looks. By using the same file as valiases,
|
||||
# every alias is owned (uniquely) by its user.
|
||||
# The user's own address is already in all_valiases_postfix.
|
||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (
|
||||
lookupTableToString regex_valiases_postfix
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit
|
||||
valiases_file
|
||||
regex_valiases_file
|
||||
vaccounts_file
|
||||
regex_vaccounts_file
|
||||
mailserverExtraVirtualAliases
|
||||
;
|
||||
}
|
|
@ -14,6 +14,12 @@ in
|
|||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
};
|
||||
postmaster = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
abuse = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
};
|
||||
imports = [
|
||||
inputs.simple-nixos-mailserver.nixosModules.mailserver
|
||||
|
|
|
@ -192,6 +192,8 @@ in
|
|||
'';
|
||||
restartUnits = [
|
||||
"rspamd.service"
|
||||
# See note on why there's a condition set on postfix for this to make sense.
|
||||
"postfix.service"
|
||||
];
|
||||
}
|
||||
]) cfg.domains
|
||||
|
@ -222,12 +224,18 @@ in
|
|||
# without this postfix won't forward the mails to rspamd to be signed.
|
||||
non_smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||
};
|
||||
systemd.services.rspamd = {
|
||||
unitConfig = {
|
||||
ConditionPathExists = domainKeyPaths;
|
||||
};
|
||||
serviceConfig = {
|
||||
ReadOnlyPaths = domainKeyPaths;
|
||||
systemd.services = {
|
||||
# Postfix technically does not need these files to exist. But without
|
||||
# configuring this here, it will attempt to start (ie. getting restarted) many times during startup
|
||||
# and eventually hit a limit on the number of times. So ensuring it cannot try to start, should prevent this.
|
||||
postfix.unitConfig.ConditionPathExists = domainKeyPaths;
|
||||
rspamd = {
|
||||
unitConfig = {
|
||||
ConditionPathExists = domainKeyPaths;
|
||||
};
|
||||
serviceConfig = {
|
||||
ReadOnlyPaths = domainKeyPaths;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ in
|
|||
{{- with secret "kanidm/data/apps/dovecot" -}}
|
||||
scope = email openid profile
|
||||
username_attribute = username
|
||||
debug = yes
|
||||
introspection_url = https://dovecot:{{ .Data.data.basic_secret }}@login.kaareskovgaard.net/oauth2/token/introspect
|
||||
introspection_mode = post
|
||||
{{- end -}}
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
"khs" = {
|
||||
isLdapAccount = true;
|
||||
name = "Kaare Agerlin Skovgaard";
|
||||
aliases = [
|
||||
addresses = [
|
||||
"kaare@agerlin-skovgaard.dk"
|
||||
"kaare@agerlinskovgaard.dk"
|
||||
"@k.agerlinskovgaard.dk"
|
||||
"@k.agerlin-skovgaard.dk"
|
||||
];
|
||||
quota = "10G";
|
||||
};
|
||||
|
|
|
@ -42,6 +42,6 @@ fn pem_private_key_to_sodium_private_key(p: PemPrivateKeyToSodiumPrivateKey) ->
|
|||
let libsodium_seed = &result[16..48];
|
||||
let keypair = libsodium_rs::crypto_sign::KeyPair::from_seed(libsodium_seed)?;
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write(keypair.secret_key.as_bytes())?;
|
||||
stdout.write_all(keypair.secret_key.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue