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