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