1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.vdirsyncer;
8
9 toIniJson = with generators; toINI {
10 mkKeyValue = mkKeyValueDefault {
11 mkValueString = builtins.toJSON;
12 } "=";
13 };
14
15 toConfigFile = name: cfg':
16 if
17 cfg'.configFile != null
18 then
19 cfg'.configFile
20 else
21 pkgs.writeText "vdirsyncer-${name}.conf" (toIniJson (
22 {
23 general = cfg'.config.general // (lib.optionalAttrs (cfg'.config.statusPath == null) {
24 status_path = "/var/lib/vdirsyncer/${name}";
25 });
26 } // (
27 mapAttrs' (name: nameValuePair "pair ${name}") cfg'.config.pairs
28 ) // (
29 mapAttrs' (name: nameValuePair "storage ${name}") cfg'.config.storages
30 )
31 ));
32
33 userUnitConfig = name: cfg': {
34 serviceConfig = {
35 User = if cfg'.user == null then "vdirsyncer" else cfg'.user;
36 Group = if cfg'.group == null then "vdirsyncer" else cfg'.group;
37 } // (optionalAttrs (cfg'.user == null) {
38 DynamicUser = true;
39 }) // (optionalAttrs (cfg'.additionalGroups != []) {
40 SupplementaryGroups = cfg'.additionalGroups;
41 }) // (optionalAttrs (cfg'.config.statusPath == null) {
42 StateDirectory = "vdirsyncer/${name}";
43 StateDirectoryMode = "0700";
44 });
45 };
46
47 commonUnitConfig = {
48 after = [ "network.target" ];
49 serviceConfig = {
50 Type = "oneshot";
51 # Sandboxing
52 PrivateTmp = true;
53 NoNewPrivileges = true;
54 ProtectSystem = "strict";
55 ProtectHome = true;
56 ProtectKernelTunables = true;
57 ProtectKernelModules = true;
58 ProtectControlGroups = true;
59 RestrictNamespaces = true;
60 MemoryDenyWriteExecute = true;
61 RestrictRealtime = true;
62 RestrictSUIDSGID = true;
63 RestrictAddressFamilies = "AF_INET AF_INET6";
64 LockPersonality = true;
65 };
66 };
67
68in
69{
70 options = {
71 services.vdirsyncer = {
72 enable = mkEnableOption (mdDoc "vdirsyncer");
73
74 package = mkPackageOptionMD pkgs "vdirsyncer" {};
75
76 jobs = mkOption {
77 description = mdDoc "vdirsyncer job configurations";
78 type = types.attrsOf (types.submodule {
79 options = {
80 enable = (mkEnableOption (mdDoc "this vdirsyncer job")) // {
81 default = true;
82 example = false;
83 };
84
85 user = mkOption {
86 type = types.nullOr types.str;
87 default = null;
88 description = mdDoc ''
89 User account to run vdirsyncer as, otherwise as a systemd
90 dynamic user
91 '';
92 };
93
94 group = mkOption {
95 type = types.nullOr types.str;
96 default = null;
97 description = mdDoc "group to run vdirsyncer as";
98 };
99
100 additionalGroups = mkOption {
101 type = types.listOf types.str;
102 default = [];
103 description = mdDoc "additional groups to add the dynamic user to";
104 };
105
106 forceDiscover = mkOption {
107 type = types.bool;
108 default = false;
109 description = mdDoc ''
110 Run `yes | vdirsyncer discover` prior to `vdirsyncer sync`
111 '';
112 };
113
114 timerConfig = mkOption {
115 type = types.attrs;
116 default = {
117 OnBootSec = "1h";
118 OnUnitActiveSec = "6h";
119 };
120 description = mdDoc "systemd timer configuration";
121 };
122
123 configFile = mkOption {
124 type = types.nullOr types.path;
125 default = null;
126 description = mdDoc "existing configuration file";
127 };
128
129 config = {
130 statusPath = mkOption {
131 type = types.nullOr types.str;
132 default = null;
133 defaultText = literalExpression "/var/lib/vdirsyncer/\${attrName}";
134 description = mdDoc "vdirsyncer's status path";
135 };
136
137 general = mkOption {
138 type = types.attrs;
139 default = {};
140 description = mdDoc "general configuration";
141 };
142
143 pairs = mkOption {
144 type = types.attrsOf types.attrs;
145 default = {};
146 description = mdDoc "vdirsyncer pair configurations";
147 example = literalExpression ''
148 {
149 my_contacts = {
150 a = "my_cloud_contacts";
151 b = "my_local_contacts";
152 collections = [ "from a" ];
153 conflict_resolution = "a wins";
154 metadata = [ "color" "displayname" ];
155 };
156 };
157 '';
158 };
159
160 storages = mkOption {
161 type = types.attrsOf types.attrs;
162 default = {};
163 description = mdDoc "vdirsyncer storage configurations";
164 example = literalExpression ''
165 {
166 my_cloud_contacts = {
167 type = "carddav";
168 url = "https://dav.example.com/";
169 read_only = true;
170 username = "user";
171 "password.fetch" = [ "command" "cat" "/etc/vdirsyncer/cloud.passwd" ];
172 };
173 my_local_contacts = {
174 type = "carddav";
175 url = "https://localhost/";
176 username = "user";
177 "password.fetch" = [ "command" "cat" "/etc/vdirsyncer/local.passwd" ];
178 };
179 }
180 '';
181 };
182 };
183 };
184 });
185 };
186 };
187 };
188
189 config = mkIf cfg.enable {
190 systemd.services = mapAttrs' (name: cfg': nameValuePair "vdirsyncer@${name}" (
191 foldr recursiveUpdate {} [
192 commonUnitConfig
193 (userUnitConfig name cfg')
194 {
195 description = "synchronize calendars and contacts (${name})";
196 environment.VDIRSYNCER_CONFIG = toConfigFile name cfg';
197 serviceConfig.ExecStart =
198 (optional cfg'.forceDiscover (
199 pkgs.writeShellScript "vdirsyncer-discover-yes" ''
200 set -e
201 yes | ${cfg.package}/bin/vdirsyncer discover
202 ''
203 )) ++ [ "${cfg.package}/bin/vdirsyncer sync" ];
204 }
205 ]
206 )) (filterAttrs (name: cfg': cfg'.enable) cfg.jobs);
207
208 systemd.timers = mapAttrs' (name: cfg': nameValuePair "vdirsyncer@${name}" {
209 wantedBy = [ "timers.target" ];
210 description = "synchronize calendars and contacts (${name})";
211 inherit (cfg') timerConfig;
212 }) cfg.jobs;
213 };
214}