treewide: implement auto importing for modules

Reduce maintenance burden and increase efficiency by automatically
importing modules following a specific convention.

Co-authored-by: awwpotato <awwpotato@voidq.com>
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
Austin Horstman 2025-06-20 14:39:55 -05:00
parent fefeb0e928
commit 4fca600cb1
461 changed files with 72 additions and 474 deletions

View file

@ -0,0 +1,267 @@
{ lib, ... }:
let
inherit (lib) literalExpression mkOption types;
extraConfigType =
with lib.types;
attrsOf (oneOf [
str
int
bool
(listOf str)
]);
perAccountGroups =
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
# Make value of name the same as the name used with the dot prefix
default = name;
readOnly = true;
description = ''
The name of this group for this account. These names are different than
some others, because they will hide channel names that are the same.
'';
};
channels = mkOption {
type = types.attrsOf (types.submodule channel);
default = { };
description = ''
List of channels that should be grouped together into this group. When
performing a synchronization, the groups are synchronized, rather than
the individual channels.
Using these channels and then grouping them together allows for you to
define the maildir hierarchy as you see fit.
'';
};
};
};
# Options for configuring channel(s) that will be composed together into a group.
channel =
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
default = name;
readOnly = true;
description = ''
The unique name for THIS channel in THIS group. The group will refer to
this channel by this name.
In addition, you can manually sync just this channel by specifying this
name to mbsync on the command line.
'';
};
farPattern = mkOption {
type = types.str;
default = "";
example = "[Gmail]/Sent Mail";
description = ''
IMAP4 patterns for which mailboxes on the remote mail server to sync.
If `Patterns` are specified, `farPattern`
is interpreted as a prefix which is not matched against the patterns,
and is not affected by mailbox list overrides.
If this is left as the default, then mbsync will default to the pattern
`INBOX`.
'';
};
nearPattern = mkOption {
type = types.str;
default = "";
example = "Sent";
description = ''
Name for where mail coming from the remote (far) mail server will end up
locally. The mailbox specified by the far pattern will be placed in
this directory.
If this is left as the default, then mbsync will default to the pattern
`INBOX`.
'';
};
patterns = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "INBOX" ];
description = ''
Instead of synchronizing *just* the mailboxes that
match the `farPattern`, use it as a prefix which is
not matched against the patterns, and is not affected by mailbox list
overrides.
'';
};
extraConfig = mkOption {
type = extraConfigType;
default = { };
example = literalExpression ''
{
Create = "both";
CopyArrivalDate = "yes";
MaxMessages = 10000;
MaxSize = "1m";
}
'';
description = ''
Extra configuration lines to add to *THIS* channel's
configuration.
'';
};
};
};
in
{
options.mbsync = {
enable = lib.mkEnableOption "synchronization using mbsync";
flatten = mkOption {
type = types.nullOr types.str;
default = null;
example = ".";
description = ''
If set, flattens the hierarchy within the maildir by
substituting the canonical hierarchy delimiter
`/` with this value.
'';
};
subFolders = mkOption {
type = types.enum [
"Verbatim"
"Maildir++"
"Legacy"
];
default = "Verbatim";
example = "Maildir++";
description = ''
The on-disk folder naming style. This option has no
effect when {option}`flatten` is used.
'';
};
create = mkOption {
type = types.enum [
"none"
"maildir"
"imap"
"both"
];
default = "none";
example = "maildir";
description = ''
Automatically create missing mailboxes within the
given mail store.
'';
};
remove = mkOption {
type = types.enum [
"none"
"maildir"
"imap"
"both"
];
default = "none";
example = "imap";
description = ''
Propagate mailbox deletions to the given mail store.
'';
};
expunge = mkOption {
type = types.enum [
"none"
"maildir"
"imap"
"both"
];
default = "none";
example = "both";
description = ''
Permanently remove messages marked for deletion from
the given mail store.
'';
};
patterns = mkOption {
type = types.listOf types.str;
default = [ "*" ];
description = ''
Pattern of mailboxes to synchronize.
'';
};
groups = mkOption {
type = types.attrsOf (types.submodule perAccountGroups);
default = { };
# The default cannot actually be empty, but contains an attribute set where
# the channels set is empty. If a group is specified, then a name is given,
# creating the attribute set.
description = ''
Some email providers (Gmail) have a different directory hierarchy for
synchronized email messages. Namely, when using mbsync without specifying
a set of channels into a group, all synchronized directories end up beneath
the `[Gmail]/` directory.
This option allows you to specify a group, and subsequently channels that
will allow you to sync your mail into an arbitrary hierarchy.
'';
};
extraConfig.channel = mkOption {
type = extraConfigType;
default = { };
example = literalExpression ''
{
MaxMessages = 10000;
MaxSize = "1m";
};
'';
description = ''
Per channel extra configuration.
'';
};
extraConfig.local = mkOption {
type = extraConfigType;
default = { };
description = ''
Local store extra configuration.
'';
};
extraConfig.remote = mkOption {
type = extraConfigType;
default = { };
description = ''
Remote store extra configuration.
'';
};
extraConfig.account = mkOption {
type = extraConfigType;
default = { };
example = literalExpression ''
{
TLSType = "IMAP";
TLSVersions = [ "+1.3" "+1.2" "-1.1" ];
PipelineDepth = 10;
Timeout = 60;
};
'';
description = ''
Account section extra configuration.
'';
};
};
}

View file

@ -0,0 +1,361 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
concatStringsSep
concatMapStringsSep
literalExpression
mapAttrsToList
mkIf
mkOption
optionalAttrs
types
;
cfg = config.programs.mbsync;
# Accounts for which mbsync is enabled.
mbsyncAccounts = lib.filter (a: a.mbsync.enable) (lib.attrValues config.accounts.email.accounts);
# Given a SINGLE group's channels attribute set, return true if ANY of the channel's
# patterns use the invalidOption attribute set value name.
channelInvalidOption =
channels: invalidOption: any (c: c) (mapAttrsToList (c: lib.hasAttr invalidOption) channels);
# Given a SINGLE account's groups attribute set, return true if ANY of the account's group's channel's patterns use the invalidOption attribute set value name.
groupInvalidOption =
groups: invalidOption:
any (g: g) (
mapAttrsToList (groupName: groupVals: channelInvalidOption groupVals.channels invalidOption) groups
);
# Given all accounts (ensure that accounts passed in here ARE mbsync-using accounts)
# return true if ANY of the account's groups' channels' patterns use the
# invalidOption attribute set value name.
accountInvalidOption =
accounts: invalidOption:
any (a: a) (map (account: groupInvalidOption account.mbsync.groups invalidOption) mbsyncAccounts);
genTlsConfig =
tls:
{
TLSType =
if !tls.enable then
"None"
else if tls.useStartTls then
"STARTTLS"
else
"IMAPS";
}
// lib.optionalAttrs (tls.enable && tls.certificatesFile != null) {
CertificateFile = toString tls.certificatesFile;
};
nearFarMapping = {
none = "None";
imap = "Far";
maildir = "Near";
both = "Both";
};
genSection =
header: entries:
let
escapeValue = lib.escape [ ''"'' ];
hasSpace = v: builtins.match ".* .*" v != null;
genValue =
n: v:
if lib.isList v then
concatMapStringsSep " " (genValue n) v
else if lib.isBool v then
lib.hm.booleans.yesNo v
else if lib.isInt v then
toString v
else if lib.isString v && hasSpace v then
''"${escapeValue v}"''
else if lib.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:
let
inherit (account)
name
maildir
imap
mbsync
passwordCommand
userName
;
in
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" (
{
Inbox = "${maildir.absPath}/${account.folders.inbox}";
}
// optionalAttrs (mbsync.subFolders != "Maildir++" || mbsync.flatten != null) {
Path = "${maildir.absPath}/";
}
// optionalAttrs (mbsync.flatten == null) {
SubFolders = mbsync.subFolders;
}
// optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; }
// mbsync.extraConfig.local
)
+ "\n"
+ genChannels account;
genChannels =
account:
let
inherit (account) name mbsync;
in
if mbsync.groups == { } then
genAccountWideChannel account
else
genGroupChannelConfig name mbsync.groups + "\n" + genAccountGroups mbsync.groups;
# Used when no channels are specified for this account. This will create a
# single channel for the entire account that is then further refined within
# the Group for synchronization.
genAccountWideChannel =
account:
let
inherit (account) name mbsync;
in
genSection "Channel ${name}" (
{
Far = ":${name}-remote:";
Near = ":${name}-local:";
Patterns = mbsync.patterns;
Create = nearFarMapping.${mbsync.create};
Remove = nearFarMapping.${mbsync.remove};
Expunge = nearFarMapping.${mbsync.expunge};
SyncState = "*";
}
// mbsync.extraConfig.channel
)
+ "\n";
# 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
# "far" 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 = lib.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 (lib.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}" (
{
Far = ":${storeName}-remote:${channel.farPattern}";
Near = ":${storeName}-local:${channel.nearPattern}";
}
// channel.extraConfig
)
+ genChannelPatterns channel.patterns;
# 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:
lib.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: concatStringsSep "\n" (genChannelStrings group.name group.channels);
# Generate all channel configurations for all groups for this account.
in
concatStringsSep "\n" (
lib.remove "" (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, if the group has channels specified, construct the
# "Group <grpName>" header and each of the channels.
genGroupChannelString =
group:
lib.flatten (
lib.optionals (group.channels != { }) (
[ "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 constituent channels.
genGroupsStrings = mapAttrsToList (
name: info: concatStringsSep "\n" (genGroupChannelString groups.${name})
) groups;
# Join all non-empty groups.
combined = concatStringsSep "\n\n" (lib.remove "" genGroupsStrings) + "\n";
in
combined;
genGroupConfig =
name: channels:
let
genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}";
in
"\n" + concatStringsSep "\n" ([ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels);
in
{
meta.maintainers = [ lib.maintainers.KarlJoad ];
options = {
programs.mbsync = {
enable = lib.mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer";
package = lib.mkPackageOption pkgs "isync" { };
groups = mkOption {
type = types.attrsOf (types.attrsOf (types.listOf types.str));
default = { };
example = literalExpression ''
{
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 ./accounts.nix));
};
};
config = mkIf cfg.enable (
lib.mkMerge [
{
assertions =
let
checkAccounts =
pred: msg:
let
badAccounts = lib.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")
];
}
(mkIf (accountInvalidOption mbsyncAccounts "masterPattern") {
warnings = [
"mbsync channels no longer use masterPattern. Use farPattern in its place."
];
})
(mkIf (accountInvalidOption mbsyncAccounts "slavePattern") {
warnings = [
"mbsync channels no longer use slavePattern. Use nearPattern in its place."
];
})
{
home.packages = [ cfg.package ];
programs.notmuch.new.ignore = [
".uidvalidity"
".mbsyncstate"
];
xdg.configFile."isyncrc".text =
let
accountsConfig = map genAccountConfig mbsyncAccounts;
# Only generate this kind of Group configuration if there are ANY accounts
# that do NOT have a per-account groups/channels option(s) specified.
groupsConfig =
if any (account: account.mbsync.groups == { }) mbsyncAccounts then
mapAttrsToList genGroupConfig cfg.groups
else
[ ];
in
''
# Generated by Home Manager.
''
+ concatStringsSep "\n" (lib.optional (cfg.extraConfig != "") cfg.extraConfig)
+ concatStringsSep "\n\n" accountsConfig
+ concatStringsSep "\n" groupsConfig;
home.activation = mkIf (mbsyncAccounts != [ ]) {
createMaildir = lib.hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] ''
run mkdir -m700 -p $VERBOSE_ARG ${
concatMapStringsSep " " (a: lib.escapeShellArg a.maildir.absPath) mbsyncAccounts
}
'';
};
}
]
);
}