nixos/dovecot: add settings option (RFC42)

Co-authored-by: Katalin Rebhan <me@dblsaiko.net>
This commit is contained in:
Jappie3
2026-04-11 01:11:33 +02:00
committed by Katalin Rebhan
co-authored by Katalin Rebhan
parent b5aeef3116
commit ad56a1d988
2 changed files with 336 additions and 373 deletions
+328 -367
View File
@@ -7,33 +7,45 @@
let
inherit (lib)
attrValues
all
attrsToList
concatMapStringsSep
concatStrings
concatStringsSep
elemAt
filter
flatten
imap1
isAttrs
isBool
isDerivation
isInt
isList
isPath
isString
listToAttrs
literalExpression
mapAttrs'
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
mkRemovedOptionModule
mkRenamedOptionModule
nameValuePair
optional
optionalAttrs
optionalString
optionals
singleton
splitString
traceSeq
types
mkRenamedOptionModule
nameValuePair
mapAttrs'
listToAttrs
filter
;
inherit (lib.strings) match;
cfg = config.services.dovecot2;
dovecotPkg = pkgs.dovecot;
baseDir = "/run/dovecot2";
stateDir = "/var/lib/dovecot";
@@ -69,35 +81,6 @@ let
)
);
mkExtraConfigCollisionWarning = term: ''
You referred to ${term} in `services.dovecot2.extraConfig`.
Due to gradual transition to structured configuration for plugin configuration, it is possible
this will cause your plugin configuration to be ignored.
Consider setting `services.dovecot2.pluginSettings.${term}` instead.
'';
# Those settings are automatically set based on other parts
# of this module.
automaticallySetPluginSettings = [
"sieve_plugins"
"sieve_extensions"
"sieve_global_extensions"
"sieve_pipe_bin_dir"
]
++ (builtins.attrNames sieveScriptSettings)
++ (builtins.attrNames imapSieveMailboxSettings);
# The idea is to match everything that looks like `$term =`
# but not `# $term something something`
# or `# $term = some value` because those are comments.
configContainsSetting = lines: term: (match "[[:blank:]]*${term}[[:blank:]]*=.*" lines) != null;
warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning (
filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings
);
sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" (
map (el: {
name = builtins.unsafeDiscardStringContext (baseNameOf el);
@@ -105,200 +88,263 @@ let
}) cfg.sieve.pipeBins
);
dovecotConf = concatStrings [
''
base_dir = ${baseDir}
protocols = ${concatStringsSep " " cfg.protocols}
sendmail_path = /run/wrappers/bin/sendmail
mail_plugin_dir = /run/current-system/sw/lib/dovecot/modules
# defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion
mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
''
(concatStringsSep "\n" (
mapAttrsToList (protocol: plugins: ''
protocol ${protocol} {
mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
}
'') cfg.mailPlugins.perProtocol
))
(
if cfg.sslServerCert == null then
''
ssl = no
disable_plaintext_auth = no
''
else
''
ssl_cert = <${cfg.sslServerCert}
ssl_key = <${cfg.sslServerKey}
${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
${optionalString cfg.enableDHE "ssl_dh = <${config.security.dhparams.params.dovecot2.path}"}
disable_plaintext_auth = yes
''
)
''
default_internal_user = ${cfg.user}
default_internal_group = ${cfg.group}
${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
mail_location = ${cfg.mailLocation}
maildir_copy_with_hardlinks = yes
pop3_uidl_format = %08Xv%08Xu
auth_mechanisms = plain login
service auth {
user = root
}
''
(optionalString cfg.enablePAM ''
userdb {
driver = passwd
}
passdb {
driver = pam
args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
}
'')
(optionalString (cfg.mailboxes != { }) ''
namespace inbox {
inbox=yes
${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
}
'')
(optionalString cfg.enableQuota ''
service quota-status {
executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
inet_listener {
port = ${cfg.quotaPort}
}
client_limit = 1
}
plugin {
quota_rule = *:storage=${cfg.quotaGlobalPerUser}
quota = count:User quota # per virtual mail user quota
quota_status_success = DUNNO
quota_status_nouser = DUNNO
quota_status_overquota = "552 5.2.2 Mailbox is full"
quota_grace = 10%%
quota_vsizes = yes
}
'')
# General plugin settings:
# - sieve is mostly generated here, refer to `pluginSettings` to follow
# the control flow.
''
plugin {
${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)}
}
''
cfg.extraConfig
allSieveSettings = mkMerge [
imapSieveMailboxSettings
sieveScriptSettings
{
sieve_plugins =
mkIf (cfg.sieve.plugins != [ ]) # .
(concatStringsSep " " cfg.sieve.plugins);
sieve_extensions =
mkIf (cfg.sieve.extensions != [ ]) # .
(concatMapStringsSep " " (el: "+${el}") cfg.sieve.extensions);
sieve_global_extensions =
mkIf (cfg.sieve.globalExtensions != [ ]) # .
(concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions);
sieve_pipe_bin_dir =
mkIf (cfg.sieve.pipeBins != [ ]) # .
sievePipeBinScriptDirectory;
}
];
mailboxConfig =
mailbox:
''
mailbox "${mailbox.name}" {
auto = ${toString mailbox.auto}
''
+ optionalString (mailbox.autoexpunge != null) ''
autoexpunge = ${mailbox.autoexpunge}
''
+ optionalString (mailbox.specialUse != null) ''
special_use = \${toString mailbox.specialUse}
''
+ "}";
yesOrNo = v: if v then "yes" else "no";
mailboxes =
{ name, ... }:
{
options = {
name = mkOption {
type = types.strMatching ''[^"]+'';
example = "Spam";
default = name;
readOnly = true;
description = "The name of the mailbox.";
};
auto = mkOption {
type = types.enum [
"no"
"create"
"subscribe"
];
default = "no";
example = "subscribe";
description = "Whether to automatically create or create and subscribe to the mailbox or not.";
};
specialUse = mkOption {
type = types.nullOr (
types.enum [
"All"
"Archive"
"Drafts"
"Flagged"
"Junk"
"Sent"
"Trash"
]
);
default = null;
example = "Junk";
description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
};
autoexpunge = mkOption {
type = types.nullOr types.str;
default = null;
example = "60d";
description = ''
To automatically remove all email from the mailbox which is older than the
specified time.
'';
};
};
};
toOption =
i: n: v:
"${i}${toString n} = ${v}";
formatKeyValue =
indent: n: v:
if (v == null) then
""
else if isInt v then
toOption indent n (toString v)
else if isBool v then
toOption indent n (yesOrNo v)
else if isString v then
toOption indent n v
else if isList v then
if all isString v then
toOption indent n (concatStringsSep " " v)
else
map (formatKeyValue indent n) v
else if isPath v || isDerivation v then
# paths -> copy to store
# derivations -> just use output path instead of looping over the attrs
toOption indent n "${v}"
else if isAttrs v then
let
sectionType = v._section.type;
sectionName = v._section.name;
sectionTitle = concatStringsSep " " (
filter (s: s != null) [
sectionType
sectionName
]
);
in
concatStringsSep "\n" (
[
"${indent}${sectionTitle} {"
]
++ (mapAttrsToList (formatKeyValue "${indent} ") (removeAttrs v [ "_section" ]))
++ [ "${indent}}" ]
)
else
throw (traceSeq v "services.dovecot2.settings: unexpected type");
doveConf = concatStringsSep "\n" (
optionals (cfg.includeFiles != [ ]) (map (f: "!include ${f}") cfg.includeFiles)
++ flatten (mapAttrsToList (formatKeyValue "") cfg.settings)
);
in
{
imports = [
(mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
(mkRemovedOptionModule [
"services"
"dovecot2"
"modules"
] "Now need to use `environment.systemPackages` to load additional Dovecot modules")
(mkRemovedOptionModule [
"services"
"dovecot2"
"enablePop3"
] "Set 'services.dovecot2.settings.protocols.pop3 = true/false;' instead.")
(mkRemovedOptionModule [
"services"
"dovecot2"
"enableImap"
] "Set 'services.dovecot2.settings.protocols.imap = true/false;' instead.")
(mkRemovedOptionModule [
"services"
"dovecot2"
"enableLmtp"
] "Set 'services.dovecot2.settings.protocols.lmtp = true/false;' instead.")
(mkRenamedOptionModule
[ "services" "dovecot2" "sieveScripts" ]
[ "services" "dovecot2" "sieve" "scripts" ]
)
];
(mkRenamedOptionModule
[ "services" "dovecot2" "mailUser" ]
[ "services" "dovecot2" "settings" "mail_uid" ]
)
(mkRenamedOptionModule
[ "services" "dovecot2" "mailGroup" ]
[ "services" "dovecot2" "settings" "mail_gid" ]
)
(mkRenamedOptionModule
[ "services" "dovecot2" "protocols" ]
[ "services" "dovecot2" "settings" "protocols" ]
)
]
++ (
let
basePath = [
"services"
"dovecot2"
];
mkRemovedOptions =
list:
map (
name: mkRemovedOptionModule (basePath ++ name) "Please use services.dovecot2.settings instead."
) list;
in
mkRemovedOptions [
[ "extraConfig" ]
[ "mailboxes" ]
[ "pluginSettings" ]
[ "enableQuota" ]
[ "quotaPort" ]
[ "quotaGlobalPerUser" ]
]
);
options.services.dovecot2 = {
enable = mkEnableOption "the dovecot 2.x POP3/IMAP server";
enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled)";
enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)" // {
default = true;
package = mkPackageOption pkgs "dovecot" { } // {
};
enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled)";
settings = mkOption {
default = { };
type =
let
inherit (lib.types)
attrsOf
path
bool
int
nonEmptyListOf
nonEmptyStr
nullOr
oneOf
str
submodule
;
inherit (lib.lists) last dropEnd;
protocols = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Additional listeners to start when Dovecot is enabled.";
section = submodule (
{ name, options, ... }:
let
# if the current name is a list (matches '[definition .*]') -> get
# name' from _module.args.loc & use it for {type,name}Default
name' =
if (builtins.match "[[]definition .*].*" name) == null then
name
else
last (dropEnd 3 options._module.args.loc);
# split name' on the first space
splits = builtins.match "([^ ]+) (.+)" name';
typeDefault = if splits == null then name' else builtins.elemAt splits 0;
nameDefault = if splits == null then null else builtins.elemAt splits 1;
in
{
options = {
_section = {
type = mkOption {
description = "Section type, mandatory for every section.";
type = nonEmptyStr;
default = typeDefault;
};
name = mkOption {
description = "Section name, comes after section type & is optional in some cases.";
type = nullOr nonEmptyStr;
default = nameDefault;
};
};
};
freeformType = attrsOf valueType;
}
);
primitiveType = oneOf [
int
str
bool
# path must order before section, otherwise the latter will
# interpret path literals as a module path to load
path
section
];
valueType =
nullOr (oneOf [
primitiveType
(nonEmptyListOf primitiveType)
])
// {
description = "Dovecot config value";
};
in
attrsOf valueType;
description = ''
Dovecot configuration, see <https://doc.dovecot.org/latest/core/summaries/settings.html#all-dovecot-settings>
for all available options.
For information on the configuration structure, see <https://doc.dovecot.org/latest/core/settings/syntax.html>.
::: {.warning}
Explicit settings in [{option}`services.dovecot2.settings`](#opt-services.dovecot2.settings) can silently override values set by other `services.dovecot2.*` options.
:::
'';
example = {
protocols = {
imap = true;
submission = true;
lmtp = true;
};
mail_driver = "maildir";
"namespace inbox" = {
inbox = true;
separator = "/";
};
service = [
{
_section.name = "imap";
process_min_avail = 1;
client_limit = 100;
"inet_listener imap".port = 31143;
"inet_listener imaps".port = 31993;
}
{
_section.name = "lmtp";
user = "dovemail";
"unix_listener lmtp" = {
mode = "0660";
user = "postfix";
};
}
];
"protocol imap".mail_plugins = {
imap_sieve = true;
imap_filter_sieve = true;
};
mail_attribute."dict file" = {
path = "%{home}/dovecot-attributes";
};
};
};
user = mkOption {
@@ -313,13 +359,6 @@ in
description = "Dovecot group name.";
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = "mail_debug = yes";
description = "Additional entries to put verbatim into Dovecot's config file.";
};
mailPlugins =
let
plugins =
@@ -374,129 +413,31 @@ in
type = types.nullOr types.path;
default = null;
description = "Config file used for the whole dovecot configuration.";
apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
apply = v: if v != null then v else pkgs.writeText "dovecot.conf" doveConf;
};
mailLocation = mkOption {
type = types.str;
default = "maildir:/var/spool/mail/%u"; # Same as inbox, as postfix
example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
description = ''
Location that dovecot will use for mail folders. Dovecot mail_location option.
'';
};
mailUser = mkOption {
type = types.nullOr types.str;
default = null;
description = "Default user to store mail for virtual users.";
};
mailGroup = mkOption {
type = types.nullOr types.str;
default = null;
description = "Default group to store mail for virtual users.";
includeFiles = mkOption {
type = types.listOf types.path;
default = [ ];
description = "Files to include in the Dovecot config file using !include directives.";
example = [ "/foo/bar/extraDovecotConfig.conf" ];
};
createMailUser =
mkEnableOption ''
automatically creating the user
given in {option}`services.dovecot.user` and the group
given in {option}`services.dovecot.group`''
given in {option}`services.dovecot2.settings.mail_uid` and the group
given in {option}`services.dovecot2.settings.mail_gid`''
// {
default = true;
};
sslCACert = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to the server's CA certificate key.";
};
sslServerCert = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to the server's public key.";
};
sslServerKey = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to the server's private key.";
};
enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins" // {
default = true;
};
enableDHE = mkEnableOption "ssl_dh and generation of primes for the key exchange" // {
default = true;
};
showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW)";
mailboxes = mkOption {
type =
with types;
coercedTo (listOf unspecified) (
list:
listToAttrs (
map (entry: {
name = entry.name;
value = removeAttrs entry [ "name" ];
}) list
)
) (attrsOf (submodule mailboxes));
default = { };
example = literalExpression ''
{
Spam = { specialUse = "Junk"; auto = "create"; };
}
'';
description = "Configure mailboxes and auto create or subscribe them.";
};
enableQuota = mkEnableOption "the dovecot quota service";
quotaPort = mkOption {
type = types.str;
default = "12340";
description = ''
The Port the dovecot quota service binds to.
If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
'';
};
quotaGlobalPerUser = mkOption {
type = types.str;
default = "100G";
example = "10G";
description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
};
pluginSettings = mkOption {
# types.str does not coerce from packages, like `sievePipeBinScriptDirectory`.
type = types.attrsOf (
types.oneOf [
types.str
types.package
]
);
default = { };
example = literalExpression ''
{
sieve = "file:~/sieve;active=~/.dovecot.sieve";
}
'';
description = ''
Plugin settings for dovecot in general, e.g. `sieve`, `sieve_default`, etc.
Some of the other knobs of this module will influence by default the plugin settings, but you
can still override any plugin settings.
If you override a plugin setting, its value is cleared and you have to copy over the defaults.
'';
};
imapsieve.mailbox = mkOption {
default = [ ];
description = "Configure Sieve filtering rules on IMAP actions";
@@ -624,36 +565,56 @@ in
config = mkIf cfg.enable {
security.pam.services.dovecot2 = mkIf cfg.enablePAM { };
security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
enable = true;
params.dovecot2 = { };
};
services.dovecot2 = {
protocols =
optional cfg.enableImap "imap" ++ optional cfg.enablePop3 "pop3" ++ optional cfg.enableLmtp "lmtp";
mailPlugins = mkIf cfg.enableQuota {
globally.enable = [ "quota" ];
perProtocol.imap.enable = [ "imap_quota" ];
};
sieve.plugins =
optional (cfg.imapsieve.mailbox != [ ]) "sieve_imapsieve"
++ optional (cfg.sieve.pipeBins != [ ]) "sieve_extprograms";
sieve.globalExtensions = optional (cfg.sieve.pipeBins != [ ]) "vnd.dovecot.pipe";
pluginSettings = lib.mapAttrs (n: lib.mkDefault) (
settings = mkMerge [
{
sieve_plugins = concatStringsSep " " cfg.sieve.plugins;
sieve_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions);
sieve_global_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions);
sieve_pipe_bin_dir = sievePipeBinScriptDirectory;
base_dir = mkDefault baseDir;
sendmail_path = mkDefault "/run/wrappers/bin/sendmail";
mail_plugin_dir = mkDefault "/run/current-system/sw/lib/dovecot/modules";
default_internal_user = mkDefault cfg.user;
default_internal_group = mkDefault cfg.group;
maildir_copy_with_hardlinks = mkDefault true;
pop3_uidl_format = mkDefault "%08Xv%08Xu";
auth_mechanisms = mkDefault "plain login";
"service auth" = {
user = "root";
};
mail_location = mkDefault "maildir:/var/spool/mail/%u";
passdb = mkIf cfg.enablePAM (mkDefault {
driver = "pam";
args = "${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2";
});
userdb = mkIf cfg.enablePAM (mkDefault {
driver = "passwd";
});
mail_plugins = mkDefault "$mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}";
plugin = allSieveSettings;
}
// sieveScriptSettings
// imapSieveMailboxSettings
);
(mkIf (cfg.mailPlugins.perProtocol != { } || cfg.mailPlugins.globally.enable != [ ]) (
listToAttrs (
map (m: {
name = "protocol ${elemAt (splitString "." m.name) 0}";
value.mail_plugins = "$mail_plugins ${
concatStringsSep " " (m.value.enable ++ cfg.mailPlugins.globally.enable)
}";
}) (attrsToList cfg.mailPlugins.perProtocol)
)
))
];
};
users.users = {
@@ -670,12 +631,14 @@ in
group = cfg.group;
};
}
// optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
${cfg.mailUser} = {
// optionalAttrs (cfg.settings.mail_uid or null != null && cfg.createMailUser) {
${cfg.settings.mail_uid} = {
description = "Virtual Mail User";
isSystemUser = true;
}
// optionalAttrs (cfg.mailGroup != null) { group = cfg.mailGroup; };
// optionalAttrs (cfg.settings.mail_gid or null != null) {
group = cfg.settings.mail_gid;
};
};
users.groups = {
@@ -684,8 +647,8 @@ in
// optionalAttrs (cfg.group == "dovecot2") {
dovecot2.gid = config.ids.gids.dovecot2;
}
// optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
${cfg.mailGroup} = { };
// optionalAttrs (cfg.settings.mail_gid or null != null && cfg.createMailUser) {
${cfg.settings.mail_gid} = { };
};
environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
@@ -704,8 +667,8 @@ in
startLimitIntervalSec = 60; # 1 min
serviceConfig = {
Type = "notify";
ExecStart = "${dovecotPkg}/sbin/dovecot -F";
ExecReload = "${dovecotPkg}/sbin/doveadm reload";
ExecStart = "${lib.getExe cfg.package} -F";
ExecReload = "${lib.getExe' cfg.package "doveadm"} reload";
CapabilityBoundingSet = [
"CAP_CHOWN"
@@ -773,7 +736,9 @@ in
${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
'') cfg.sieve.scripts
)}
chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
${optionalString (
cfg.settings ? mail_uid && cfg.settings.mail_uid != null && cfg.settings.mail_gid != null
) "chown -R '${cfg.settings.mail_uid}:${cfg.settings.mail_gid}' '${stateDir}/sieve'"}
''
+ optionalString (cfg.imapsieve.mailbox != [ ]) ''
mkdir -p ${stateDir}/imapsieve/{before,after}
@@ -791,29 +756,22 @@ in
) cfg.imapsieve.mailbox}
${optionalString (
cfg.mailUser != null && cfg.mailGroup != null
) "chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/imapsieve'"}
cfg.settings ? mail_uid && cfg.settings.mail_uid != null && cfg.settings.mail_gid != null
) "chown -R '${cfg.settings.mail_uid}:${cfg.settings.mail_gid}' '${stateDir}/imapsieve'"}
'';
};
environment.systemPackages = [ dovecotPkg ];
warnings = warnAboutExtraConfigCollisions;
environment.systemPackages = [ cfg.package ];
assertions = [
{
assertion =
(cfg.sslServerCert == null) == (cfg.sslServerKey == null)
&& (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
}
{
assertion = cfg.showPAMFailure -> cfg.enablePAM;
message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
}
{
assertion = cfg.sieve.scripts != { } -> (cfg.mailUser != null && cfg.mailGroup != null);
message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set";
assertion =
cfg.sieve.scripts != { } -> (cfg.settings.mail_uid != null && cfg.settings.mail_gid != null);
message = "dovecot requires settings.mail_uid and settings.mail_gid to be set when `sieve.scripts` is set";
}
{
assertion = config.systemd.services ? dovecot2 == false;
@@ -822,8 +780,11 @@ in
'';
}
];
};
meta.maintainers = [ lib.maintainers.dblsaiko ];
meta.maintainers = with lib.maintainers; [
dblsaiko
jappie3
prince213
];
}
+8 -6
View File
@@ -8,12 +8,14 @@
services.postfix.enable = true;
services.dovecot2 = {
enable = true;
protocols = [
"imap"
"pop3"
];
mailUser = "vmail";
mailGroup = "vmail";
settings = {
protocols = [
"imap"
"pop3"
];
mail_uid = "vmail";
mail_gid = "vmail";
};
};
environment.systemPackages =
let