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