home-manager/modules/programs/mbsync.nix
Karl Hallsby 82264461e5 mbsync: only generate group/channel configuration if channels present
Typically, when a group is specified, channels will be specified as
well. However, if due to error or mistake, the user forgets to specify
ANY channels for a group, we should not generate that group's
information.

This means that no channels are specified (which maps the remote
master to local slave). In addition, the `Group <gName>` block (which
brings the separate channels together) is also not generated.

Another thing to consider is that a user might specify a group and a
channel, but perform no additional configuration of the channel.
In a configuration, this would be realized by
`accounts.email.accounts.<aName>.mbsync.groups.<gName>.channels.<cName>;`

This creates the channel with the name `<cName>` and the
`masterPattern`, `slavePattern`, and `patterns` fields use their defaults.
By definitions set within mbsync, these defaults actually specify that
the remote master's `INBOX` mail directory is synchronized to the
local slave's `INBOX` directory.

So, if there is a channel that has no fields specified, then we DO
want to generate its configuration. But if there is a group that has
no channels, then we do NOT generate it.
2020-06-27 15:26:11 +02:00

221 lines
7.8 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.mbsync;
# Accounts for which mbsync is enabled.
mbsyncAccounts =
filter (a: a.mbsync.enable) (attrValues config.accounts.email.accounts);
genTlsConfig = tls:
{
SSLType = if !tls.enable then
"None"
else if tls.useStartTls then
"STARTTLS"
else
"IMAPS";
} // optionalAttrs (tls.enable && tls.certificatesFile != null) {
CertificateFile = toString tls.certificatesFile;
};
masterSlaveMapping = {
none = "None";
imap = "Master";
maildir = "Slave";
both = "Both";
};
genSection = header: entries:
let
escapeValue = escape [ ''"'' ];
hasSpace = v: builtins.match ".* .*" v != null;
genValue = n: v:
if isList v then
concatMapStringsSep " " (genValue n) v
else if isBool v then
(if v then "yes" else "no")
else if isInt v then
toString v
else if isString v && hasSpace v then
''"${escapeValue v}"''
else if isString v then
v
else
let prettyV = lib.generators.toPretty { } v;
in throw "mbsync: unexpected value for option ${n}: '${prettyV}'";
in ''
${header}
${concatStringsSep "\n"
(mapAttrsToList (n: v: "${n} ${genValue n v}") entries)}
'';
genAccountConfig = account:
with account;
genSection "IMAPAccount ${name}" ({
Host = imap.host;
User = userName;
PassCmd = toString passwordCommand;
} // genTlsConfig imap.tls
// optionalAttrs (imap.port != null) { Port = toString imap.port; }
// mbsync.extraConfig.account) + "\n"
+ genSection "IMAPStore ${name}-remote"
({ Account = name; } // mbsync.extraConfig.remote) + "\n"
+ genSection "MaildirStore ${name}-local" ({
Path = "${maildir.absPath}/";
Inbox = "${maildir.absPath}/${folders.inbox}";
SubFolders = "Verbatim";
} // optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; }
// mbsync.extraConfig.local) + "\n"
+ genGroupChannelConfig name mbsync.groups
+ genAccountGroups mbsync.groups;
# Given the attr set of groups, return a string of channels that will direct
# mail to the proper directories, according to the pattern used in channel's
# master pattern definition.
genGroupChannelConfig = storeName: groups:
let
# Given the name of the group this channel is part of and the channel
# itself, generate the string for the desired configuration.
genChannelString = groupName: channel:
let
escapeValue = escape [ ''\"'' ];
hasSpace = v: builtins.match ".* .*" v != null;
# Given a list of patterns, will return the string requested.
# Only prints if the pattern is NOT the empty list, the default.
genChannelPatterns = patterns: if (length patterns) != 0 then
"Pattern " + concatStringsSep " " (map
(pat: if hasSpace pat then escapeValue pat else pat) patterns) + "\n"
else "";
in
genSection "Channel ${groupName}-${channel.name}" ({
Master = ":${storeName}-remote:${channel.masterPattern}";
Slave = ":${storeName}-local:${channel.slavePattern}";
} // channel.extraConfig)
+ genChannelPatterns channel.patterns
+ "\n";
# Given the group name, and a attr set of channels within that group,
# Generate a list of strings for each channels' configuration.
genChannelStrings = groupName: channels: optionals (channels != { })
(mapAttrsToList (channelName: info: genChannelString groupName info) channels);
# Given a group, return a string that configures all the channels within
# the group.
genGroupsChannels = group: concatStrings
(genChannelStrings group.name group.channels);
in
# Generate all channel configurations for all groups for this account.
concatStringsSep "\n"
(mapAttrsToList (name: group: genGroupsChannels group) groups);
# Given the attr set of groups, return a string which maps channels to groups
genAccountGroups = groups:
let
# Given the name of the group and the attribute set of channels, make
# make "Channel <grpName>-<chnName>" for each channel to list os strings
genChannelStrings = groupName: channels: mapAttrsToList
(name: info: "Channel ${groupName}-${name}") channels;
# Take in 1 group, construct the "Group <grpName>" header, and if the group
# is NOT empty, construct each of the channels.
genGroupChannelString = group: optionals (groups != { })
([("Group " + group.name)] ++
(genChannelStrings group.name group.channels));
# Given set of groups, generates list of strings, where each string is one
# of the groups and its consituent channels.
genGroupsStrings = mapAttrsToList (name: info: concatStringsSep "\n"
(genGroupChannelString groups.${name})) groups;
in (concatStringsSep "\n\n" genGroupsStrings) # Put all strings together.
# 2 \n needed in concatStringsSep because last element genGroupsStrings
# has no \n.
+ "\n\n"; # Additional spacing after this account's group setup.
genGroupConfig = name: channels:
let
genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}";
in concatStringsSep "\n"
([ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels);
in {
options = {
programs.mbsync = {
enable = mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer";
package = mkOption {
type = types.package;
default = pkgs.isync;
defaultText = literalExample "pkgs.isync";
example = literalExample "pkgs.isync";
description = "The package to use for the mbsync binary.";
};
groups = mkOption {
type = types.attrsOf (types.attrsOf (types.listOf types.str));
default = { };
example = literalExample ''
{
inboxes = {
account1 = [ "Inbox" ];
account2 = [ "Inbox" ];
};
}
'';
description = ''
Definition of groups.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration lines to add to the mbsync configuration.
'';
};
};
accounts.email.accounts = mkOption {
type = with types; attrsOf (submodule (import ./mbsync-accounts.nix));
};
};
config = mkIf cfg.enable {
assertions = let
checkAccounts = pred: msg:
let badAccounts = filter pred mbsyncAccounts;
in {
assertion = badAccounts == [ ];
message = "mbsync: ${msg} for accounts: "
+ concatMapStringsSep ", " (a: a.name) badAccounts;
};
in [
(checkAccounts (a: a.maildir == null) "Missing maildir configuration")
(checkAccounts (a: a.imap == null) "Missing IMAP configuration")
(checkAccounts (a: a.passwordCommand == null) "Missing passwordCommand")
(checkAccounts (a: a.userName == null) "Missing username")
];
home.packages = [ cfg.package ];
programs.notmuch.new.ignore = [ ".uidvalidity" ".mbsyncstate" ];
home.file.".mbsyncrc".text = let
accountsConfig = map genAccountConfig mbsyncAccounts;
groupsConfig = mapAttrsToList genGroupConfig cfg.groups;
in concatStringsSep "\n" ([''
# Generated by Home Manager.
''] ++ optional (cfg.extraConfig != "") cfg.extraConfig ++ accountsConfig
++ groupsConfig) + "\n";
home.activation = mkIf (mbsyncAccounts != [ ]) {
createMaildir =
hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] ''
$DRY_RUN_CMD mkdir -m700 -p $VERBOSE_ARG ${
concatMapStringsSep " " (a: a.maildir.absPath) mbsyncAccounts
}
'';
};
};
}