1{ lib, config, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.roundcube;
7 fpm = config.services.phpfpm.pools.roundcube;
8 localDB = cfg.database.host == "localhost";
9 user = cfg.database.username;
10 phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
11in
12{
13 options.services.roundcube = {
14 enable = mkOption {
15 type = types.bool;
16 default = false;
17 description = lib.mdDoc ''
18 Whether to enable roundcube.
19
20 Also enables nginx virtual host management.
21 Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
22 See [](#opt-services.nginx.virtualHosts) for further information.
23 '';
24 };
25
26 hostName = mkOption {
27 type = types.str;
28 example = "webmail.example.com";
29 description = lib.mdDoc "Hostname to use for the nginx vhost";
30 };
31
32 package = mkOption {
33 type = types.package;
34 default = pkgs.roundcube;
35 defaultText = literalExpression "pkgs.roundcube";
36
37 example = literalExpression ''
38 roundcube.withPlugins (plugins: [ plugins.persistent_login ])
39 '';
40
41 description = lib.mdDoc ''
42 The package which contains roundcube's sources. Can be overridden to create
43 an environment which contains roundcube and third-party plugins.
44 '';
45 };
46
47 database = {
48 username = mkOption {
49 type = types.str;
50 default = "roundcube";
51 description = lib.mdDoc ''
52 Username for the postgresql connection.
53 If `database.host` is set to `localhost`, a unix user and group of the same name will be created as well.
54 '';
55 };
56 host = mkOption {
57 type = types.str;
58 default = "localhost";
59 description = lib.mdDoc ''
60 Host of the postgresql server. If this is not set to
61 `localhost`, you have to create the
62 postgresql user and database yourself, with appropriate
63 permissions.
64 '';
65 };
66 password = mkOption {
67 type = types.str;
68 description = lib.mdDoc "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use `passwordFile` instead.";
69 default = "";
70 };
71 passwordFile = mkOption {
72 type = types.str;
73 description = lib.mdDoc ''
74 Password file for the postgresql connection.
75 Must be formatted according to PostgreSQL .pgpass standard (see https://www.postgresql.org/docs/current/libpq-pgpass.html)
76 but only one line, no comments and readable by user `nginx`.
77 Ignored if `database.host` is set to `localhost`, as peer authentication will be used.
78 '';
79 };
80 dbname = mkOption {
81 type = types.str;
82 default = "roundcube";
83 description = lib.mdDoc "Name of the postgresql database";
84 };
85 };
86
87 plugins = mkOption {
88 type = types.listOf types.str;
89 default = [];
90 description = lib.mdDoc ''
91 List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
92 '';
93 };
94
95 dicts = mkOption {
96 type = types.listOf types.package;
97 default = [];
98 example = literalExpression "with pkgs.aspellDicts; [ en fr de ]";
99 description = lib.mdDoc ''
100 List of aspell dictionaries for spell checking. If empty, spell checking is disabled.
101 '';
102 };
103
104 maxAttachmentSize = mkOption {
105 type = types.int;
106 default = 18;
107 description = lib.mdDoc ''
108 The maximum attachment size in MB.
109
110 Note: Since roundcube only uses 70% of max upload values configured in php
111 30% is added automatically to [](#opt-services.roundcube.maxAttachmentSize).
112 '';
113 apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.3)}M";
114 };
115
116 extraConfig = mkOption {
117 type = types.lines;
118 default = "";
119 description = lib.mdDoc "Extra configuration for roundcube webmail instance";
120 };
121 };
122
123 config = mkIf cfg.enable {
124 # backward compatibility: if password is set but not passwordFile, make one.
125 services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
126 warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
127
128 environment.etc."roundcube/config.inc.php".text = ''
129 <?php
130
131 ${lib.optionalString (!localDB) ''
132 $password = file('${cfg.database.passwordFile}')[0];
133 $password = preg_split('~\\\\.(*SKIP)(*FAIL)|\:~s', $password);
134 $password = end($password);
135 $password = str_replace("\\:", ":", $password);
136 $password = str_replace("\\\\", "\\", $password);
137 ''}
138
139 $config = array();
140 $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
141 $config['log_driver'] = 'syslog';
142 $config['max_message_size'] = '${cfg.maxAttachmentSize}';
143 $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
144 $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
145 $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
146 # Roundcube uses PHP-FPM which has `PrivateTmp = true;`
147 $config['temp_dir'] = '/tmp';
148 $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
149 # by default, spellchecking uses a third-party cloud services
150 $config['spellcheck_engine'] = 'pspell';
151 $config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
152
153 ${cfg.extraConfig}
154 '';
155
156 services.nginx = {
157 enable = true;
158 virtualHosts = {
159 ${cfg.hostName} = {
160 forceSSL = mkDefault true;
161 enableACME = mkDefault true;
162 locations."/" = {
163 root = cfg.package;
164 index = "index.php";
165 extraConfig = ''
166 location ~* \.php(/|$) {
167 fastcgi_split_path_info ^(.+\.php)(/.+)$;
168 fastcgi_pass unix:${fpm.socket};
169
170 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
171 fastcgi_param PATH_INFO $fastcgi_path_info;
172
173 include ${config.services.nginx.package}/conf/fastcgi_params;
174 include ${pkgs.nginx}/conf/fastcgi.conf;
175 }
176 '';
177 };
178 };
179 };
180 };
181
182 services.postgresql = mkIf localDB {
183 enable = true;
184 ensureDatabases = [ cfg.database.dbname ];
185 ensureUsers = [ {
186 name = cfg.database.username;
187 ensurePermissions = {
188 "DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
189 };
190 } ];
191 };
192
193 users.users.${user} = mkIf localDB {
194 group = user;
195 isSystemUser = true;
196 createHome = false;
197 };
198 users.groups.${user} = mkIf localDB {};
199
200 services.phpfpm.pools.roundcube = {
201 user = if localDB then user else "nginx";
202 phpOptions = ''
203 error_log = 'stderr'
204 log_errors = on
205 post_max_size = ${cfg.maxAttachmentSize}
206 upload_max_filesize = ${cfg.maxAttachmentSize}
207 '';
208 settings = mapAttrs (name: mkDefault) {
209 "listen.owner" = "nginx";
210 "listen.group" = "nginx";
211 "listen.mode" = "0660";
212 "pm" = "dynamic";
213 "pm.max_children" = 75;
214 "pm.start_servers" = 2;
215 "pm.min_spare_servers" = 1;
216 "pm.max_spare_servers" = 20;
217 "pm.max_requests" = 500;
218 "catch_workers_output" = true;
219 };
220 phpPackage = phpWithPspell;
221 phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
222 };
223 systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
224
225 # Restart on config changes.
226 systemd.services.phpfpm-roundcube.restartTriggers = [
227 config.environment.etc."roundcube/config.inc.php".source
228 ];
229
230 systemd.services.roundcube-setup = mkMerge [
231 (mkIf (cfg.database.host == "localhost") {
232 requires = [ "postgresql.service" ];
233 after = [ "postgresql.service" ];
234 path = [ config.services.postgresql.package ];
235 })
236 {
237 after = [ "network-online.target" ];
238 wantedBy = [ "multi-user.target" ];
239 script = let
240 psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
241 in
242 ''
243 version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
244 if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
245 ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
246 fi
247
248 if [ ! -f /var/lib/roundcube/des_key ]; then
249 base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
250 # we need to log out everyone in case change the des_key
251 # from the default when upgrading from nixos 19.09
252 ${psql} <<< 'TRUNCATE TABLE session;'
253 fi
254
255 ${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
256 '';
257 serviceConfig = {
258 Type = "oneshot";
259 StateDirectory = "roundcube";
260 User = if localDB then user else "nginx";
261 # so that the des_key is not world readable
262 StateDirectoryMode = "0700";
263 };
264 }
265 ];
266 };
267}