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