diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8f6b504af..128ad52e3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -76,6 +76,9 @@
/modules/programs/matplotlib.nix @rprospero
+/modules/programs/mbsync.nix @KarlJoad
+/tests/modules/programs/mbsync @KarlJoad
+
/modules/programs/mcfly.nix @marsam
/modules/programs/mpv.nix @tadeokondrak
diff --git a/modules/programs/mbsync-accounts.nix b/modules/programs/mbsync-accounts.nix
index 4de1965fe..c1bd551fa 100644
--- a/modules/programs/mbsync-accounts.nix
+++ b/modules/programs/mbsync-accounts.nix
@@ -1,4 +1,4 @@
-{ lib, ... }:
+{ config, lib, ... }:
with lib;
@@ -6,6 +6,110 @@ let
extraConfigType = with lib.types; attrsOf (either (either str int) bool);
+ perAccountGroups = { name, config, ... }: {
+ 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, config, ... }: {
+ 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.
+ '';
+ };
+
+ masterPattern = 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, masterPattern
+ 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.
+ '';
+ };
+
+ slavePattern = mkOption {
+ type = types.str;
+ default = "";
+ example = "Sent";
+ description = ''
+ Name for where mail coming from the master mail server will end up
+ locally. The mailbox specified by the master's 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 masterPattern, 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 = ''
+ {
+ Create = "both";
+ CopyArrivalDate = "yes";
+ MaxMessages = 10000;
+ MaxSize = "1m";
+ }
+ '';
+ description = ''
+ Extra configuration lines to add to THIS channel's
+ configuration.
+ '';
+ };
+ };
+ };
+
in {
options.mbsync = {
enable = mkEnableOption "synchronization using mbsync";
@@ -58,6 +162,23 @@ in {
'';
};
+ 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 = { };
diff --git a/modules/programs/mbsync.nix b/modules/programs/mbsync.nix
index f2814b393..f9713da3a 100644
--- a/modules/programs/mbsync.nix
+++ b/modules/programs/mbsync.nix
@@ -69,23 +69,98 @@ let
Inbox = "${maildir.absPath}/${folders.inbox}";
SubFolders = "Verbatim";
} // optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; }
- // mbsync.extraConfig.local) + "\n" + genSection "Channel ${name}" ({
- Master = ":${name}-remote:";
- Slave = ":${name}-local:";
- Patterns = mbsync.patterns;
- Create = masterSlaveMapping.${mbsync.create};
- Remove = masterSlaveMapping.${mbsync.remove};
- Expunge = masterSlaveMapping.${mbsync.expunge};
- SyncState = "*";
- } // mbsync.extraConfig.channel) + "\n";
+ // mbsync.extraConfig.local) + "\n" + genChannels account;
+
+ genChannels = account:
+ with account;
+ 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:
+ with account;
+ genSection "Channel ${name}" ({
+ Master = ":${name}-remote:";
+ Slave = ":${name}-local:";
+ Patterns = mbsync.patterns;
+ Create = masterSlaveMapping.${mbsync.create};
+ Remove = masterSlaveMapping.${mbsync.remove};
+ Expunge = masterSlaveMapping.${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
+ # 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;
+ # 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:
+ concatStringsSep "\n" (genChannelStrings group.name group.channels);
+ # Generate all channel configurations for all groups for this account.
+ in concatStringsSep "\n" (filter (s: s != "")
+ (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 -" 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 " header and each of the channels.
+ genGroupChannelString = group:
+ flatten (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 consituent channels.
+ genGroupsStrings = mapAttrsToList (name: info:
+ concatStringsSep "\n" (genGroupChannelString groups.${name})) groups;
+ in concatStringsSep "\n\n" (filter (s: s != "")
+ genGroupsStrings) # filter for the cases of empty groups
+ + "\n"; # Put all strings together.
genGroupConfig = name: channels:
let
genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}";
- in concatStringsSep "\n"
+ in "\n" + concatStringsSep "\n"
([ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels);
in {
+ meta.maintainers = [ maintainers.KarlJoad ];
+
options = {
programs.mbsync = {
enable = mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer";
@@ -150,11 +225,20 @@ in {
home.file.".mbsyncrc".text = let
accountsConfig = map genAccountConfig mbsyncAccounts;
- groupsConfig = mapAttrsToList genGroupConfig cfg.groups;
- in concatStringsSep "\n" ([''
+ # 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.
- ''] ++ optional (cfg.extraConfig != "") cfg.extraConfig ++ accountsConfig
- ++ groupsConfig) + "\n";
+
+ ''
+ + concatStringsSep "\n" (optional (cfg.extraConfig != "") cfg.extraConfig)
+ + concatStringsSep "\n\n" accountsConfig
+ + concatStringsSep "\n" groupsConfig;
home.activation = mkIf (mbsyncAccounts != [ ]) {
createMaildir =
diff --git a/tests/modules/programs/mbsync/mbsync-expected.conf b/tests/modules/programs/mbsync/mbsync-expected.conf
index f1ca79fe7..672296047 100644
--- a/tests/modules/programs/mbsync/mbsync-expected.conf
+++ b/tests/modules/programs/mbsync/mbsync-expected.conf
@@ -15,14 +15,41 @@ Inbox /home/hm-user/Mail/hm-account/Inbox
Path /home/hm-user/Mail/hm-account/
SubFolders Verbatim
-Channel hm-account
-Create None
-Expunge None
+Channel emptyChannels-empty1
Master :hm-account-remote:
-Patterns *
-Remove None
Slave :hm-account-local:
-SyncState *
+
+Channel emptyChannels-empty2
+Master :hm-account-remote:
+Slave :hm-account-local:
+
+Channel hm-account-earlierPatternMatch
+Master :hm-account-remote:Label
+Slave :hm-account-local:SomethingUnderLabel
+Pattern ThingUnderLabel !NotThisMaildirThough "[Weird] Label?"
+
+Channel hm-account-inbox
+Master :hm-account-remote:Inbox
+Slave :hm-account-local:Inbox
+
+Channel hm-account-patternMatch
+Master :hm-account-remote:Label
+Slave :hm-account-local:SomethingUnderLabel
+Pattern ThingUnderLabel !NotThisMaildirThough "[Weird] Label?"
+
+Channel hm-account-strangeHostBoxName
+Master ":hm-account-remote:[Weird]/Label Mess"
+Slave :hm-account-local:[AnotherWeird]/Label
+
+Group emptyChannels
+Channel emptyChannels-empty1
+Channel emptyChannels-empty2
+
+Group hm-account
+Channel hm-account-earlierPatternMatch
+Channel hm-account-inbox
+Channel hm-account-patternMatch
+Channel hm-account-strangeHostBoxName
IMAPAccount hm@example.com
@@ -40,16 +67,14 @@ Inbox /home/hm-user/Mail/hm@example.com/Inbox
Path /home/hm-user/Mail/hm@example.com/
SubFolders Verbatim
-Channel hm@example.com
-Create None
-Expunge None
-Master :hm@example.com-remote:
-Patterns *
-Remove None
-Slave :hm@example.com-local:
-SyncState *
+Channel inboxes-inbox1
+Master :hm@example.com-remote:Inbox1
+Slave :hm@example.com-local:Inboxes
+Channel inboxes-inbox2
+Master :hm@example.com-remote:Inbox2
+Slave :hm@example.com-local:Inboxes
Group inboxes
-Channel hm-account:Inbox
-Channel hm@example.com:Inbox1,Inbox2
+Channel inboxes-inbox1
+Channel inboxes-inbox2
diff --git a/tests/modules/programs/mbsync/mbsync.nix b/tests/modules/programs/mbsync/mbsync.nix
index fa8e28cb4..a6e555cd4 100644
--- a/tests/modules/programs/mbsync/mbsync.nix
+++ b/tests/modules/programs/mbsync/mbsync.nix
@@ -8,6 +8,10 @@ with lib;
config = {
programs.mbsync = {
enable = true;
+ # programs.mbsync.groups and
+ # accounts.email.accounts..mbsync.groups should NOT be used at the
+ # same time.
+ # If they are, then the new version will take precendence.
groups.inboxes = {
"hm@example.com" = [ "Inbox1" "Inbox2" ];
hm-account = [ "Inbox" ];
@@ -15,9 +19,60 @@ with lib;
};
accounts.email.accounts = {
- "hm@example.com".mbsync = { enable = true; };
+ "hm@example.com".mbsync = {
+ enable = true;
+ groups.inboxes = {
+ channels = {
+ inbox1 = {
+ masterPattern = "Inbox1";
+ slavePattern = "Inboxes";
+ };
+ inbox2 = {
+ masterPattern = "Inbox2";
+ slavePattern = "Inboxes";
+ };
+ };
+ };
+ };
- hm-account.mbsync = { enable = true; };
+ hm-account.mbsync = {
+ enable = true;
+ groups.hm-account = {
+ channels.earlierPatternMatch = {
+ masterPattern = "Label";
+ slavePattern = "SomethingUnderLabel";
+ patterns = [
+ "ThingUnderLabel"
+ "!NotThisMaildirThough"
+ ''"[Weird] Label?"''
+ ];
+ };
+ channels.inbox = {
+ masterPattern = "Inbox";
+ slavePattern = "Inbox";
+ };
+ channels.strangeHostBoxName = {
+ masterPattern = "[Weird]/Label Mess";
+ slavePattern = "[AnotherWeird]/Label";
+ };
+ channels.patternMatch = {
+ masterPattern = "Label";
+ slavePattern = "SomethingUnderLabel";
+ patterns = [
+ "ThingUnderLabel"
+ "!NotThisMaildirThough"
+ ''"[Weird] Label?"''
+ ];
+ };
+ };
+ # No group should be printed.
+ groups.emptyGroup = { };
+ # Group should be printed, but left with default channels.
+ groups.emptyChannels = {
+ channels.empty1 = { };
+ channels.empty2 = { };
+ };
+ };
};
nmt.script = ''