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