1 { lib, config, pkgs, ... }:
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);
10 options.services.roundcube = {
11 enable = lib.mkOption {
12 type = lib.types.bool;
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.
23 hostName = lib.mkOption {
25 example = "webmail.example.com";
26 description = "Hostname to use for the nginx vhost";
29 package = lib.mkPackageOption pkgs "roundcube" {
30 example = "roundcube.withPlugins (plugins: [ plugins.persistent_login ])";
34 username = lib.mkOption {
36 default = "roundcube";
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.
44 default = "localhost";
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
52 password = lib.mkOption {
54 description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use `passwordFile` instead.";
57 passwordFile = lib.mkOption {
60 Password file for the postgresql connection.
61 Must be formatted according to PostgreSQL .pgpass standard (see https://www.postgresql.org/docs/current/libpq-pgpass.html)
62 but only one line, no comments and readable by user `nginx`.
63 Ignored if `database.host` is set to `localhost`, as peer authentication will be used.
66 dbname = lib.mkOption {
68 default = "roundcube";
69 description = "Name of the postgresql database";
73 plugins = lib.mkOption {
74 type = lib.types.listOf lib.types.str;
77 List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
81 dicts = lib.mkOption {
82 type = lib.types.listOf lib.types.package;
84 example = lib.literalExpression "with pkgs.aspellDicts; [ en fr de ]";
86 List of aspell dictionaries for spell checking. If empty, spell checking is disabled.
90 maxAttachmentSize = lib.mkOption {
93 apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.37)}M";
95 The maximum attachment size in MB.
96 [upstream issue comment]: https://github.com/roundcube/roundcubemail/issues/7979#issuecomment-808879209
98 Since there is some overhead in base64 encoding applied to attachments, + 37% will be added
99 to the value set in this option in order to offset the overhead. For example, setting
100 `maxAttachmentSize` to `100` would result in `137M` being the real value in the configuration.
101 See [upstream issue comment] for more details on the motivations behind this.
106 configureNginx = lib.mkOption {
107 type = lib.types.bool;
109 description = "Configure nginx as a reverse proxy for roundcube.";
112 extraConfig = lib.mkOption {
113 type = lib.types.lines;
115 description = "Extra configuration for roundcube webmail instance";
119 config = lib.mkIf cfg.enable {
120 # backward compatibility: if password is set but not passwordFile, make one.
121 services.roundcube.database.passwordFile = lib.mkIf (!localDB && cfg.database.password != "") (lib.mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
122 warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
124 environment.etc."roundcube/config.inc.php".text = ''
127 ${lib.optionalString (!localDB) ''
128 $password = file('${cfg.database.passwordFile}')[0];
129 $password = preg_split('~\\\\.(*SKIP)(*FAIL)|\:~s', $password);
130 $password = rtrim(end($password));
131 $password = str_replace("\\:", ":", $password);
132 $password = str_replace("\\\\", "\\", $password);
136 $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
137 $config['log_driver'] = 'syslog';
138 $config['max_message_size'] = '${cfg.maxAttachmentSize}';
139 $config['plugins'] = [${lib.concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
140 $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
141 $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
142 # Roundcube uses PHP-FPM which has `PrivateTmp = true;`
143 $config['temp_dir'] = '/tmp';
144 $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
145 # by default, spellchecking uses a third-party cloud services
146 $config['spellcheck_engine'] = 'pspell';
147 $config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
152 services.nginx = lib.mkIf cfg.configureNginx {
156 forceSSL = lib.mkDefault true;
157 enableACME = lib.mkDefault true;
163 add_header Cache-Control 'public, max-age=604800, must-revalidate';
166 locations."~ ^/(SQL|bin|config|logs|temp|vendor)/" = {
172 locations."~ ^/(CHANGELOG.md|INSTALL|LICENSE|README.md|SECURITY.md|UPGRADING|composer.json|composer.lock)" = {
178 locations."~* \\.php(/|$)" = {
181 fastcgi_pass unix:${fpm.socket};
182 fastcgi_param PATH_INFO $fastcgi_path_info;
183 fastcgi_split_path_info ^(.+\.php)(/.+)$;
184 include ${config.services.nginx.package}/conf/fastcgi.conf;
193 assertion = localDB -> cfg.database.username == cfg.database.dbname;
195 When setting up a DB and its owner user, the owner and the DB name must be
201 services.postgresql = lib.mkIf localDB {
203 ensureDatabases = [ cfg.database.dbname ];
205 name = cfg.database.username;
206 ensureDBOwnership = true;
210 users.users.${user} = lib.mkIf localDB {
215 users.groups.${user} = lib.mkIf localDB {};
217 services.phpfpm.pools.roundcube = {
218 user = if localDB then user else "nginx";
222 post_max_size = ${cfg.maxAttachmentSize}
223 upload_max_filesize = ${cfg.maxAttachmentSize}
225 settings = lib.mapAttrs (name: lib.mkDefault) {
226 "listen.owner" = "nginx";
227 "listen.group" = "nginx";
228 "listen.mode" = "0660";
230 "pm.max_children" = 75;
231 "pm.start_servers" = 2;
232 "pm.min_spare_servers" = 1;
233 "pm.max_spare_servers" = 20;
234 "pm.max_requests" = 500;
235 "catch_workers_output" = true;
237 phpPackage = phpWithPspell;
238 phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
240 systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
242 # Restart on config changes.
243 systemd.services.phpfpm-roundcube.restartTriggers = [
244 config.environment.etc."roundcube/config.inc.php".source
247 systemd.services.roundcube-setup = lib.mkMerge [
248 (lib.mkIf (cfg.database.host == "localhost") {
249 requires = [ "postgresql.service" ];
250 after = [ "postgresql.service" ];
253 wants = [ "network-online.target" ];
254 after = [ "network-online.target" ];
255 wantedBy = [ "multi-user.target" ];
257 path = [ config.services.postgresql.package ];
259 psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
262 version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
263 if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
264 ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
267 if [ ! -f /var/lib/roundcube/des_key ]; then
268 base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
269 # we need to log out everyone in case change the des_key
270 # from the default when upgrading from nixos 19.09
271 ${psql} <<< 'TRUNCATE TABLE session;'
274 ${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
278 StateDirectory = "roundcube";
279 User = if localDB then user else "nginx";
280 # so that the des_key is not world readable
281 StateDirectoryMode = "0700";