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