1{ lib, config, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.postfixadmin;
7 fpm = config.services.phpfpm.pools.postfixadmin;
8 localDB = cfg.database.host == "localhost";
9 user = if localDB then cfg.database.username else "nginx";
10in
11{
12 options.services.postfixadmin = {
13 enable = mkOption {
14 type = types.bool;
15 default = false;
16 description = lib.mdDoc ''
17 Whether to enable postfixadmin.
18
19 Also enables nginx virtual host management.
20 Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
21 See [](#opt-services.nginx.virtualHosts) for further information.
22 '';
23 };
24
25 hostName = mkOption {
26 type = types.str;
27 example = "postfixadmin.example.com";
28 description = lib.mdDoc "Hostname to use for the nginx vhost";
29 };
30
31 adminEmail = mkOption {
32 type = types.str;
33 example = "postmaster@example.com";
34 description = lib.mdDoc ''
35 Defines the Site Admin's email address.
36 This will be used to send emails from to create mailboxes and
37 from Send Email / Broadcast message pages.
38 '';
39 };
40
41 setupPasswordFile = mkOption {
42 type = types.path;
43 description = lib.mdDoc ''
44 Password file for the admin.
45 Generate with `php -r "echo password_hash('some password here', PASSWORD_DEFAULT);"`
46 '';
47 };
48
49 database = {
50 username = mkOption {
51 type = types.str;
52 default = "postfixadmin";
53 description = lib.mdDoc ''
54 Username for the postgresql connection.
55 If `database.host` is set to `localhost`, a unix user and group of the same name will be created as well.
56 '';
57 };
58 host = mkOption {
59 type = types.str;
60 default = "localhost";
61 description = lib.mdDoc ''
62 Host of the postgresql server. If this is not set to
63 `localhost`, you have to create the
64 postgresql user and database yourself, with appropriate
65 permissions.
66 '';
67 };
68 passwordFile = mkOption {
69 type = types.path;
70 description = lib.mdDoc "Password file for the postgresql connection. Must be readable by user `nginx`.";
71 };
72 dbname = mkOption {
73 type = types.str;
74 default = "postfixadmin";
75 description = lib.mdDoc "Name of the postgresql database";
76 };
77 };
78
79 extraConfig = mkOption {
80 type = types.lines;
81 default = "";
82 description = lib.mdDoc "Extra configuration for the postfixadmin instance, see postfixadmin's config.inc.php for available options.";
83 };
84 };
85
86 config = mkIf cfg.enable {
87 environment.etc."postfixadmin/config.local.php".text = ''
88 <?php
89
90 $CONF['setup_password'] = file_get_contents('${cfg.setupPasswordFile}');
91
92 $CONF['database_type'] = 'pgsql';
93 $CONF['database_host'] = ${if localDB then "null" else "'${cfg.database.host}'"};
94 ${optionalString localDB "$CONF['database_user'] = '${cfg.database.username}';"}
95 $CONF['database_password'] = ${if localDB then "'dummy'" else "file_get_contents('${cfg.database.passwordFile}')"};
96 $CONF['database_name'] = '${cfg.database.dbname}';
97 $CONF['configured'] = true;
98
99 ${cfg.extraConfig}
100 '';
101
102 systemd.tmpfiles.rules = [ "d /var/cache/postfixadmin/templates_c 700 ${user} ${user}" ];
103
104 services.nginx = {
105 enable = true;
106 virtualHosts = {
107 ${cfg.hostName} = {
108 forceSSL = mkDefault true;
109 enableACME = mkDefault true;
110 locations."/" = {
111 root = "${pkgs.postfixadmin}/public";
112 index = "index.php";
113 extraConfig = ''
114 location ~* \.php$ {
115 fastcgi_split_path_info ^(.+\.php)(/.+)$;
116 fastcgi_pass unix:${fpm.socket};
117 include ${config.services.nginx.package}/conf/fastcgi_params;
118 include ${pkgs.nginx}/conf/fastcgi.conf;
119 }
120 '';
121 };
122 };
123 };
124 };
125
126 services.postgresql = mkIf localDB {
127 enable = true;
128 ensureUsers = [ {
129 name = cfg.database.username;
130 } ];
131 };
132 # The postgresql module doesn't currently support concepts like
133 # objects owners and extensions; for now we tack on what's needed
134 # here.
135 systemd.services.postfixadmin-postgres = let pgsql = config.services.postgresql; in mkIf localDB {
136 after = [ "postgresql.service" ];
137 bindsTo = [ "postgresql.service" ];
138 wantedBy = [ "multi-user.target" ];
139 path = [
140 pgsql.package
141 pkgs.util-linux
142 ];
143 script = ''
144 set -eu
145
146 PSQL() {
147 psql --port=${toString pgsql.port} "$@"
148 }
149
150 PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.database.dbname}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.database.dbname}" OWNER "${cfg.database.username}"'
151 current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.database.dbname}'")
152 if [[ "$current_owner" != "${cfg.database.username}" ]]; then
153 PSQL -tAc 'ALTER DATABASE "${cfg.database.dbname}" OWNER TO "${cfg.database.username}"'
154 if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}" ]]; then
155 echo "Reassigning ownership of database ${cfg.database.dbname} to user ${cfg.database.username} failed on last boot. Failing..."
156 exit 1
157 fi
158 touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
159 PSQL "${cfg.database.dbname}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.database.username}\""
160 rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
161 fi
162 '';
163
164 serviceConfig = {
165 User = pgsql.superUser;
166 Type = "oneshot";
167 RemainAfterExit = true;
168 };
169 };
170
171 users.users.${user} = mkIf localDB {
172 group = user;
173 isSystemUser = true;
174 createHome = false;
175 };
176 users.groups.${user} = mkIf localDB {};
177
178 services.phpfpm.pools.postfixadmin = {
179 user = user;
180 phpPackage = pkgs.php81;
181 phpOptions = ''
182 error_log = 'stderr'
183 log_errors = on
184 '';
185 settings = mapAttrs (name: mkDefault) {
186 "listen.owner" = "nginx";
187 "listen.group" = "nginx";
188 "listen.mode" = "0660";
189 "pm" = "dynamic";
190 "pm.max_children" = 75;
191 "pm.start_servers" = 2;
192 "pm.min_spare_servers" = 1;
193 "pm.max_spare_servers" = 20;
194 "pm.max_requests" = 500;
195 "catch_workers_output" = true;
196 };
197 };
198 };
199}