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