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.php80.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 overriden 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 "Password file for the postgresql connection. Must be readable by user `nginx`. Ignored if `database.host` is set to `localhost`, as peer authentication will be used.";
74 };
75 dbname = mkOption {
76 type = types.str;
77 default = "roundcube";
78 description = lib.mdDoc "Name of the postgresql database";
79 };
80 };
81
82 plugins = mkOption {
83 type = types.listOf types.str;
84 default = [];
85 description = lib.mdDoc ''
86 List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
87 '';
88 };
89
90 dicts = mkOption {
91 type = types.listOf types.package;
92 default = [];
93 example = literalExpression "with pkgs.aspellDicts; [ en fr de ]";
94 description = lib.mdDoc ''
95 List of aspell dictionnaries for spell checking. If empty, spell checking is disabled.
96 '';
97 };
98
99 maxAttachmentSize = mkOption {
100 type = types.int;
101 default = 18;
102 description = lib.mdDoc ''
103 The maximum attachment size in MB.
104
105 Note: Since roundcube only uses 70% of max upload values configured in php
106 30% is added automatically to [](#opt-services.roundcube.maxAttachmentSize).
107 '';
108 apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.3)}M";
109 };
110
111 extraConfig = mkOption {
112 type = types.lines;
113 default = "";
114 description = lib.mdDoc "Extra configuration for roundcube webmail instance";
115 };
116 };
117
118 config = mkIf cfg.enable {
119 # backward compatibility: if password is set but not passwordFile, make one.
120 services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
121 warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
122
123 environment.etc."roundcube/config.inc.php".text = ''
124 <?php
125
126 ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
127
128 $config = array();
129 $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
130 $config['log_driver'] = 'syslog';
131 $config['max_message_size'] = '${cfg.maxAttachmentSize}';
132 $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
133 $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
134 $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
135 $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
136 # by default, spellchecking uses a third-party cloud services
137 $config['spellcheck_engine'] = 'pspell';
138 $config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
139
140 ${cfg.extraConfig}
141 '';
142
143 services.nginx = {
144 enable = true;
145 virtualHosts = {
146 ${cfg.hostName} = {
147 forceSSL = mkDefault true;
148 enableACME = mkDefault true;
149 locations."/" = {
150 root = cfg.package;
151 index = "index.php";
152 extraConfig = ''
153 location ~* \.php$ {
154 fastcgi_split_path_info ^(.+\.php)(/.+)$;
155 fastcgi_pass unix:${fpm.socket};
156 include ${config.services.nginx.package}/conf/fastcgi_params;
157 include ${pkgs.nginx}/conf/fastcgi.conf;
158 }
159 '';
160 };
161 };
162 };
163 };
164
165 services.postgresql = mkIf localDB {
166 enable = true;
167 ensureDatabases = [ cfg.database.dbname ];
168 ensureUsers = [ {
169 name = cfg.database.username;
170 ensurePermissions = {
171 "DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
172 };
173 } ];
174 };
175
176 users.users.${user} = mkIf localDB {
177 group = user;
178 isSystemUser = true;
179 createHome = false;
180 };
181 users.groups.${user} = mkIf localDB {};
182
183 services.phpfpm.pools.roundcube = {
184 user = if localDB then user else "nginx";
185 phpOptions = ''
186 error_log = 'stderr'
187 log_errors = on
188 post_max_size = ${cfg.maxAttachmentSize}
189 upload_max_filesize = ${cfg.maxAttachmentSize}
190 '';
191 settings = mapAttrs (name: mkDefault) {
192 "listen.owner" = "nginx";
193 "listen.group" = "nginx";
194 "listen.mode" = "0660";
195 "pm" = "dynamic";
196 "pm.max_children" = 75;
197 "pm.start_servers" = 2;
198 "pm.min_spare_servers" = 1;
199 "pm.max_spare_servers" = 20;
200 "pm.max_requests" = 500;
201 "catch_workers_output" = true;
202 };
203 phpPackage = phpWithPspell;
204 phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
205 };
206 systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
207
208 # Restart on config changes.
209 systemd.services.phpfpm-roundcube.restartTriggers = [
210 config.environment.etc."roundcube/config.inc.php".source
211 ];
212
213 systemd.services.roundcube-setup = mkMerge [
214 (mkIf (cfg.database.host == "localhost") {
215 requires = [ "postgresql.service" ];
216 after = [ "postgresql.service" ];
217 path = [ config.services.postgresql.package ];
218 })
219 {
220 wantedBy = [ "multi-user.target" ];
221 script = let
222 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}";
223 in
224 ''
225 version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
226 if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
227 ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
228 fi
229
230 if [ ! -f /var/lib/roundcube/des_key ]; then
231 base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
232 # we need to log out everyone in case change the des_key
233 # from the default when upgrading from nixos 19.09
234 ${psql} <<< 'TRUNCATE TABLE session;'
235 fi
236
237 ${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
238 '';
239 serviceConfig = {
240 Type = "oneshot";
241 StateDirectory = "roundcube";
242 User = if localDB then user else "nginx";
243 # so that the des_key is not world readable
244 StateDirectoryMode = "0700";
245 };
246 }
247 ];
248 };
249}