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