python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / mail / mailman.nix
blob198c2f3280f58e3f0d91c6af66e1aa6bd62c5f28
1 { config, pkgs, lib, ... }:          # mailman.nix
3 with lib;
5 let
7   cfg = config.services.mailman;
9   inherit (pkgs.mailmanPackages.buildEnvs { withHyperkitty = cfg.hyperkitty.enable; withLDAP = cfg.ldap.enable; })
10     mailmanEnv webEnv;
12   withPostgresql = config.services.postgresql.enable;
14   # This deliberately doesn't use recursiveUpdate so users can
15   # override the defaults.
16   webSettings = {
17     DEFAULT_FROM_EMAIL = cfg.siteOwner;
18     SERVER_EMAIL = cfg.siteOwner;
19     ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
20     COMPRESS_OFFLINE = true;
21     STATIC_ROOT = "/var/lib/mailman-web-static";
22     MEDIA_ROOT = "/var/lib/mailman-web/media";
23     LOGGING = {
24       version = 1;
25       disable_existing_loggers = true;
26       handlers.console.class = "logging.StreamHandler";
27       loggers.django = {
28         handlers = [ "console" ];
29         level = "INFO";
30       };
31     };
32     HAYSTACK_CONNECTIONS.default = {
33       ENGINE = "haystack.backends.whoosh_backend.WhooshEngine";
34       PATH = "/var/lib/mailman-web/fulltext-index";
35     };
36   } // cfg.webSettings;
38   webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
40   # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
41   postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
42     [postfix]
43     postmap_command: ${pkgs.postfix}/bin/postmap
44     transport_file_type: hash
45   '';
47   mailmanCfg = lib.generators.toINI {}
48     (recursiveUpdate cfg.settings
49       ((optionalAttrs (cfg.restApiPassFile != null) {
50         webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#";
51       })));
53   mailmanCfgFile = pkgs.writeText "mailman-raw.cfg" mailmanCfg;
55   mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
56     [general]
57     # This is your HyperKitty installation, preferably on the localhost. This
58     # address will be used by Mailman to forward incoming emails to HyperKitty
59     # for archiving. It does not need to be publicly available, in fact it's
60     # better if it is not.
61     base_url: ${cfg.hyperkitty.baseUrl}
63     # Shared API key, must be the identical to the value in HyperKitty's
64     # settings.
65     api_key: @API_KEY@
66   '';
68 in {
70   ###### interface
72   imports = [
73     (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
74       [ "services" "mailman" "hyperkitty" "baseUrl" ])
76     (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
77       The Hyperkitty API key is now generated on first run, and not
78       stored in the world-readable Nix store.  To continue using
79       Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
80     '')
81     (mkRemovedOptionModule [ "services" "mailman" "package" ] ''
82       Didn't have an effect for several years.
83     '')
84   ];
86   options = {
88     services.mailman = {
90       enable = mkOption {
91         type = types.bool;
92         default = false;
93         description = lib.mdDoc "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
94       };
96       ldap = {
97         enable = mkEnableOption (lib.mdDoc "LDAP auth");
98         serverUri = mkOption {
99           type = types.str;
100           example = "ldaps://ldap.host";
101           description = lib.mdDoc ''
102             LDAP host to connect against.
103           '';
104         };
105         bindDn = mkOption {
106           type = types.str;
107           example = "cn=root,dc=nixos,dc=org";
108           description = lib.mdDoc ''
109             Service account to bind against.
110           '';
111         };
112         bindPasswordFile = mkOption {
113           type = types.str;
114           example = "/run/secrets/ldap-bind";
115           description = lib.mdDoc ''
116             Path to the file containing the bind password of the servie account
117             defined by [](#opt-services.mailman.ldap.bindDn).
118           '';
119         };
120         superUserGroup = mkOption {
121           type = types.nullOr types.str;
122           default = null;
123           example = "cn=admin,ou=groups,dc=nixos,dc=org";
124           description = lib.mdDoc ''
125             Group where a user must be a member of to gain superuser rights.
126           '';
127         };
128         userSearch = {
129           query = mkOption {
130             type = types.str;
131             example = "(&(objectClass=inetOrgPerson)(|(uid=%(user)s)(mail=%(user)s)))";
132             description = lib.mdDoc ''
133               Query to find a user in the LDAP database.
134             '';
135           };
136           ou = mkOption {
137             type = types.str;
138             example = "ou=users,dc=nixos,dc=org";
139             description = lib.mdDoc ''
140               Organizational unit to look up a user.
141             '';
142           };
143         };
144         groupSearch = {
145           type = mkOption {
146             type = types.enum [
147               "posixGroup" "groupOfNames" "memberDNGroup" "nestedMemberDNGroup" "nestedGroupOfNames"
148               "groupOfUniqueNames" "nestedGroupOfUniqueNames" "activeDirectoryGroup" "nestedActiveDirectoryGroup"
149               "organizationalRoleGroup" "nestedOrganizationalRoleGroup"
150             ];
151             default = "posixGroup";
152             apply = v: "${toUpper (substring 0 1 v)}${substring 1 (stringLength v) v}Type";
153             description = lib.mdDoc ''
154               Type of group to perform a group search against.
155             '';
156           };
157           query = mkOption {
158             type = types.str;
159             example = "(objectClass=groupOfNames)";
160             description = lib.mdDoc ''
161               Query to find a group associated to a user in the LDAP database.
162             '';
163           };
164           ou = mkOption {
165             type = types.str;
166             example = "ou=groups,dc=nixos,dc=org";
167             description = lib.mdDoc ''
168               Organizational unit to look up a group.
169             '';
170           };
171         };
172         attrMap = {
173           username = mkOption {
174             default = "uid";
175             type = types.str;
176             description = lib.mdDoc ''
177               LDAP-attribute that corresponds to the `username`-attribute in mailman.
178             '';
179           };
180           firstName = mkOption {
181             default = "givenName";
182             type = types.str;
183             description = lib.mdDoc ''
184               LDAP-attribute that corresponds to the `firstName`-attribute in mailman.
185             '';
186           };
187           lastName = mkOption {
188             default = "sn";
189             type = types.str;
190             description = lib.mdDoc ''
191               LDAP-attribute that corresponds to the `lastName`-attribute in mailman.
192             '';
193           };
194           email = mkOption {
195             default = "mail";
196             type = types.str;
197             description = lib.mdDoc ''
198               LDAP-attribute that corresponds to the `email`-attribute in mailman.
199             '';
200           };
201         };
202       };
204       enablePostfix = mkOption {
205         type = types.bool;
206         default = true;
207         example = false;
208         description = lib.mdDoc ''
209           Enable Postfix integration. Requires an active Postfix installation.
211           If you want to use another MTA, set this option to false and configure
212           settings in services.mailman.settings.mta.
214           Refer to the Mailman manual for more info.
215         '';
216       };
218       siteOwner = mkOption {
219         type = types.str;
220         example = "postmaster@example.org";
221         description = lib.mdDoc ''
222           Certain messages that must be delivered to a human, but which can't
223           be delivered to a list owner (e.g. a bounce from a list owner), will
224           be sent to this address. It should point to a human.
225         '';
226       };
228       webHosts = mkOption {
229         type = types.listOf types.str;
230         default = [];
231         description = lib.mdDoc ''
232           The list of hostnames and/or IP addresses from which the Mailman Web
233           UI will accept requests. By default, "localhost" and "127.0.0.1" are
234           enabled. All additional names under which your web server accepts
235           requests for the UI must be listed here or incoming requests will be
236           rejected.
237         '';
238       };
240       webUser = mkOption {
241         type = types.str;
242         default = "mailman-web";
243         description = lib.mdDoc ''
244           User to run mailman-web as
245         '';
246       };
248       webSettings = mkOption {
249         type = types.attrs;
250         default = {};
251         description = lib.mdDoc ''
252           Overrides for the default mailman-web Django settings.
253         '';
254       };
256       restApiPassFile = mkOption {
257         default = null;
258         type = types.nullOr types.str;
259         description = lib.mdDoc ''
260           Path to the file containing the value for `MAILMAN_REST_API_PASS`.
261         '';
262       };
264       serve = {
265         enable = mkEnableOption (lib.mdDoc "Automatic nginx and uwsgi setup for mailman-web");
267         virtualRoot = mkOption {
268           default = "/";
269           example = lib.literalExpression "/lists";
270           type = types.str;
271           description = lib.mdDoc ''
272             Path to mount the mailman-web django application on.
273           '';
274         };
275       };
277       extraPythonPackages = mkOption {
278         description = lib.mdDoc "Packages to add to the python environment used by mailman and mailman-web";
279         type = types.listOf types.package;
280         default = [];
281       };
283       settings = mkOption {
284         description = lib.mdDoc "Settings for mailman.cfg";
285         type = types.attrsOf (types.attrsOf types.str);
286         default = {};
287       };
289       hyperkitty = {
290         enable = mkEnableOption (lib.mdDoc "the Hyperkitty archiver for Mailman");
292         baseUrl = mkOption {
293           type = types.str;
294           default = "http://localhost:18507/archives/";
295           description = lib.mdDoc ''
296             Where can Mailman connect to Hyperkitty's internal API, preferably on
297             localhost?
298           '';
299         };
300       };
302     };
303   };
305   ###### implementation
307   config = mkIf cfg.enable {
309     services.mailman.settings = {
310       mailman.site_owner = lib.mkDefault cfg.siteOwner;
311       mailman.layout = "fhs";
313       "paths.fhs" = {
314         bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
315         var_dir = "/var/lib/mailman";
316         queue_dir = "$var_dir/queue";
317         template_dir = "$var_dir/templates";
318         log_dir = "/var/log/mailman";
319         lock_dir = "$var_dir/lock";
320         etc_dir = "/etc";
321         pid_file = "/run/mailman/master.pid";
322       };
324       mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
326       "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
327         class = "mailman_hyperkitty.Archiver";
328         enable = "yes";
329         configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
330       };
331     } // (let
332       loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
333       loggerSectionNames = map (n: "logging.${n}") loggerNames;
334       in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
335     );
337     assertions = let
338       inherit (config.services) postfix;
340       requirePostfixHash = optionPath: dataFile:
341         with lib;
342         let
343           expected = "hash:/var/lib/mailman/data/${dataFile}";
344           value = attrByPath optionPath [] postfix;
345         in
346           { assertion = postfix.enable -> isList value && elem expected value;
347             message = ''
348               services.postfix.${concatStringsSep "." optionPath} must contain
349               "${expected}".
350               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
351             '';
352           };
353     in [
354       { assertion = cfg.webHosts != [];
355         message = ''
356           services.mailman.serve.enable requires there to be at least one entry
357           in services.mailman.webHosts.
358         '';
359       }
360     ] ++ (lib.optionals cfg.enablePostfix [
361       { assertion = postfix.enable;
362         message = ''
363           Mailman's default NixOS configuration requires Postfix to be enabled.
365           If you want to use another MTA, set services.mailman.enablePostfix
366           to false and configure settings in services.mailman.settings.mta.
368           Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
369           for more info.
370         '';
371       }
372       (requirePostfixHash [ "relayDomains" ] "postfix_domains")
373       (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
374       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
375     ]);
377     users.users.mailman = {
378       description = "GNU Mailman";
379       isSystemUser = true;
380       group = "mailman";
381     };
382     users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
383       description = "GNU Mailman web interface";
384       isSystemUser = true;
385       group = "mailman";
386     };
387     users.groups.mailman = {};
389     environment.etc."mailman3/settings.py".text = ''
390       import os
392       # Required by mailman_web.settings, but will be overridden when
393       # settings_local.json is loaded.
394       os.environ["SECRET_KEY"] = ""
396       from mailman_web.settings.base import *
397       from mailman_web.settings.mailman import *
399       import json
401       with open('${webSettingsJSON}') as f:
402           globals().update(json.load(f))
404       with open('/var/lib/mailman-web/settings_local.json') as f:
405           globals().update(json.load(f))
407       ${optionalString (cfg.restApiPassFile != null) ''
408         with open('${cfg.restApiPassFile}') as f:
409             MAILMAN_REST_API_PASS = f.read().rstrip('\n')
410       ''}
412       ${optionalString (cfg.ldap.enable) ''
413         import ldap
414         from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type}
415         AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}"
416         AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}"
417         with open("${cfg.ldap.bindPasswordFile}") as f:
418             AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n')
419         AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}",
420             ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}")
421         AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}()
422         AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}",
423             ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}")
424         AUTH_LDAP_USER_ATTR_MAP = {
425           ${concatStrings (flip mapAttrsToList cfg.ldap.attrMap (key: value: ''
426             "${key}": "${value}",
427           ''))}
428         }
429         ${optionalString (cfg.ldap.superUserGroup != null) ''
430           AUTH_LDAP_USER_FLAGS_BY_GROUP = {
431             "is_superuser": "${cfg.ldap.superUserGroup}"
432           }
433         ''}
434         AUTHENTICATION_BACKENDS = (
435             "django_auth_ldap.backend.LDAPBackend",
436             "django.contrib.auth.backends.ModelBackend"
437         )
438       ''}
439     '';
441     services.nginx = mkIf (cfg.serve.enable && cfg.webHosts != []) {
442       enable = mkDefault true;
443       virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
444         locations = {
445           ${cfg.serve.virtualRoot}.extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
446           "${cfg.serve.virtualRoot}/static/".alias = webSettings.STATIC_ROOT + "/";
447         };
448       });
449     };
451     environment.systemPackages = [ (pkgs.buildEnv {
452       name = "mailman-tools";
453       # We don't want to pollute the system PATH with a python
454       # interpreter etc. so let's pick only the stuff we actually
455       # want from {web,mailman}Env
456       pathsToLink = ["/bin"];
457       paths = [ mailmanEnv webEnv ];
458       # Only mailman-related stuff is installed, the rest is removed
459       # in `postBuild`.
460       ignoreCollisions = true;
461       postBuild = ''
462         find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
463       '';
464     }) ];
466     services.postfix = lib.mkIf cfg.enablePostfix {
467       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
468       config = {
469         owner_request_special = "no";   # Mailman handles -owner addresses on its own
470       };
471     };
473     systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
474       wantedBy = ["sockets.target"];
475       before = ["nginx.service"];
476       socketConfig.ListenStream = "/run/mailman-web.socket";
477     };
478     systemd.services = {
479       mailman = {
480         description = "GNU Mailman Master Process";
481         before = lib.optional cfg.enablePostfix "postfix.service";
482         after = [ "network.target" ]
483           ++ lib.optional cfg.enablePostfix "postfix-setup.service"
484           ++ lib.optional withPostgresql "postgresql.service";
485         restartTriggers = [ mailmanCfgFile ];
486         requires = optional withPostgresql "postgresql.service";
487         wantedBy = [ "multi-user.target" ];
488         serviceConfig = {
489           ExecStart = "${mailmanEnv}/bin/mailman start";
490           ExecStop = "${mailmanEnv}/bin/mailman stop";
491           User = "mailman";
492           Group = "mailman";
493           Type = "forking";
494           RuntimeDirectory = "mailman";
495           LogsDirectory = "mailman";
496           PIDFile = "/run/mailman/master.pid";
497         };
498       };
500       mailman-settings = {
501         description = "Generate settings files (including secrets) for Mailman";
502         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
503         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
504         path = with pkgs; [ jq ];
505         after = optional withPostgresql "postgresql.service";
506         requires = optional withPostgresql "postgresql.service";
507         serviceConfig.Type = "oneshot";
508         script = ''
509           install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg
510           ${optionalString (cfg.restApiPassFile != null) ''
511             ${pkgs.replace-secret}/bin/replace-secret \
512               '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
513               ${cfg.restApiPassFile} \
514               /etc/mailman.cfg
515           ''}
517           mailmanDir=/var/lib/mailman
518           mailmanWebDir=/var/lib/mailman-web
520           mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
521           mailmanWebCfg=$mailmanWebDir/settings_local.json
523           install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
524           install -m 0770 -o mailman -g mailman -d $mailmanDir
525           install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
527           if [ ! -e $mailmanWebCfg ]; then
528               hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
529               secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
531               mailmanWebCfgTmp=$(mktemp)
532               jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
533                   --arg archiver_key "$hyperkittyApiKey" \
534                   --arg secret_key "$secretKey" \
535                   >"$mailmanWebCfgTmp"
536               chown root:mailman "$mailmanWebCfgTmp"
537               chmod 440 "$mailmanWebCfgTmp"
538               mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg"
539           fi
541           hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
542           mailmanCfgTmp=$(mktemp)
543           sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
544           chown mailman:mailman "$mailmanCfgTmp"
545           mv "$mailmanCfgTmp" "$mailmanCfg"
546         '';
547       };
549       mailman-web-setup = {
550         description = "Prepare mailman-web files and database";
551         before = [ "mailman-uwsgi.service" ];
552         requiredBy = [ "mailman-uwsgi.service" ];
553         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
554         script = ''
555           [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
556           ${webEnv}/bin/mailman-web migrate
557           ${webEnv}/bin/mailman-web collectstatic
558           ${webEnv}/bin/mailman-web compress
559         '';
560         serviceConfig = {
561           User = cfg.webUser;
562           Group = "mailman";
563           Type = "oneshot";
564           WorkingDirectory = "/var/lib/mailman-web";
565         };
566       };
568       mailman-uwsgi = mkIf cfg.serve.enable (let
569         uwsgiConfig.uwsgi = {
570           type = "normal";
571           plugins = ["python3"];
572           home = webEnv;
573           manage-script-name = true;
574           mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
575           http = "127.0.0.1:18507";
576         };
577         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
578       in {
579         wantedBy = ["multi-user.target"];
580         after = optional withPostgresql "postgresql.service";
581         requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
582           ++ optional withPostgresql "postgresql.service";
583         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
584         serviceConfig = {
585           # Since the mailman-web settings.py obstinately creates a logs
586           # dir in the cwd, change to the (writable) runtime directory before
587           # starting uwsgi.
588           ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}";
589           User = cfg.webUser;
590           Group = "mailman";
591           RuntimeDirectory = "mailman-uwsgi";
592         };
593       });
595       mailman-daily = {
596         description = "Trigger daily Mailman events";
597         startAt = "daily";
598         restartTriggers = [ mailmanCfgFile ];
599         serviceConfig = {
600           ExecStart = "${mailmanEnv}/bin/mailman digests --send";
601           User = "mailman";
602           Group = "mailman";
603         };
604       };
606       hyperkitty = lib.mkIf cfg.hyperkitty.enable {
607         description = "GNU Hyperkitty QCluster Process";
608         after = [ "network.target" ];
609         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
610         wantedBy = [ "mailman.service" "multi-user.target" ];
611         serviceConfig = {
612           ExecStart = "${webEnv}/bin/mailman-web qcluster";
613           User = cfg.webUser;
614           Group = "mailman";
615           WorkingDirectory = "/var/lib/mailman-web";
616         };
617       };
618     } // flip lib.mapAttrs' {
619       "minutely" = "minutely";
620       "quarter_hourly" = "*:00/15";
621       "hourly" = "hourly";
622       "daily" = "daily";
623       "weekly" = "weekly";
624       "yearly" = "yearly";
625     } (name: startAt:
626       lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
627         description = "Trigger ${name} Hyperkitty events";
628         inherit startAt;
629         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
630         serviceConfig = {
631           ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
632           User = cfg.webUser;
633           Group = "mailman";
634           WorkingDirectory = "/var/lib/mailman-web";
635         };
636       }));
637   };
639   meta = {
640     maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
641     doc = ./mailman.xml;
642   };