diff options
19 files changed, 737 insertions, 20 deletions
diff --git a/modules/myids.nix b/modules/myids.nix index 7ec9c0e..e949ca7 100644 --- a/modules/myids.nix +++ b/modules/myids.nix | |||
@@ -3,7 +3,8 @@ | |||
3 | # Check that there is no clash with nixos/modules/misc/ids.nix | 3 | # Check that there is no clash with nixos/modules/misc/ids.nix |
4 | config = { | 4 | config = { |
5 | ids.uids = { | 5 | ids.uids = { |
6 | opendarc = 391; | 6 | vhost = 390; |
7 | openarc = 391; | ||
7 | opendmarc = 392; | 8 | opendmarc = 392; |
8 | peertube = 394; | 9 | peertube = 394; |
9 | redis = 395; | 10 | redis = 395; |
@@ -13,7 +14,8 @@ | |||
13 | mastodon = 399; | 14 | mastodon = 399; |
14 | }; | 15 | }; |
15 | ids.gids = { | 16 | ids.gids = { |
16 | opendarc = 392; | 17 | vhost = 390; |
18 | openarc = 391; | ||
17 | opendmarc = 392; | 19 | opendmarc = 392; |
18 | peertube = 394; | 20 | peertube = 394; |
19 | redis = 395; | 21 | redis = 395; |
diff --git a/modules/private/default.nix b/modules/private/default.nix index 894efb7..026e69d 100644 --- a/modules/private/default.nix +++ b/modules/private/default.nix | |||
@@ -47,6 +47,12 @@ set = { | |||
47 | peertubeTool = ./websites/tools/peertube; | 47 | peertubeTool = ./websites/tools/peertube; |
48 | toolsTool = ./websites/tools/tools; | 48 | toolsTool = ./websites/tools/tools; |
49 | 49 | ||
50 | mail = ./mail; | ||
51 | mailMilters = ./mail/milters.nix; | ||
52 | mailPostfix = ./mail/postfix.nix; | ||
53 | mailDovecot = ./mail/dovecot.nix; | ||
54 | mailRspamd = ./mail/rspamd.nix; | ||
55 | |||
50 | buildbot = ./buildbot; | 56 | buildbot = ./buildbot; |
51 | certificates = ./certificates.nix; | 57 | certificates = ./certificates.nix; |
52 | gitolite = ./gitolite; | 58 | gitolite = ./gitolite; |
@@ -55,7 +61,6 @@ set = { | |||
55 | tasks = ./tasks; | 61 | tasks = ./tasks; |
56 | dns = ./dns.nix; | 62 | dns = ./dns.nix; |
57 | ftp = ./ftp.nix; | 63 | ftp = ./ftp.nix; |
58 | mail = ./mail.nix; | ||
59 | mpd = ./mpd.nix; | 64 | mpd = ./mpd.nix; |
60 | ssh = ./ssh; | 65 | ssh = ./ssh; |
61 | 66 | ||
diff --git a/modules/private/dns.nix b/modules/private/dns.nix index f12f982..6647c14 100644 --- a/modules/private/dns.nix +++ b/modules/private/dns.nix | |||
@@ -106,7 +106,7 @@ | |||
106 | '' | 106 | '' |
107 | ; ------------------ mail: ${n} --------------------------- | 107 | ; ------------------ mail: ${n} --------------------------- |
108 | ${n} IN MX 10 mail.${conf.name}. | 108 | ${n} IN MX 10 mail.${conf.name}. |
109 | ;${n} IN MX 50 mx-1.${conf.name}. | 109 | ${n} IN MX 50 mx-1.${conf.name}. |
110 | 110 | ||
111 | ; https://tools.ietf.org/html/rfc6186 | 111 | ; https://tools.ietf.org/html/rfc6186 |
112 | _submission._tcp${suffix} SRV 0 1 587 smtp.immae.eu. | 112 | _submission._tcp${suffix} SRV 0 1 587 smtp.immae.eu. |
diff --git a/modules/private/mail/default.nix b/modules/private/mail/default.nix new file mode 100644 index 0000000..ad2c684 --- /dev/null +++ b/modules/private/mail/default.nix | |||
@@ -0,0 +1,12 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config.security.acme.certs."mail" = config.services.myCertificates.certConfig // { | ||
4 | domain = "eldiron.immae.eu"; | ||
5 | extraDomains = let | ||
6 | zonesWithMx = builtins.filter (zone: | ||
7 | lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0 | ||
8 | ) myconfig.env.dns.masterZones; | ||
9 | mxs = map (zone: "mx-1.${zone.name}") zonesWithMx; | ||
10 | in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs); | ||
11 | }; | ||
12 | } | ||
diff --git a/modules/private/mail/dovecot.nix b/modules/private/mail/dovecot.nix new file mode 100644 index 0000000..d757f59 --- /dev/null +++ b/modules/private/mail/dovecot.nix | |||
@@ -0,0 +1,255 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | let | ||
3 | sieve_bin = pkgs.runCommand "sieve_bin" { | ||
4 | buildInputs = [ pkgs.makeWrapper ]; | ||
5 | } '' | ||
6 | cp -a ${./sieve_bin} $out | ||
7 | chmod -R u+w $out | ||
8 | patchShebangs $out | ||
9 | for i in $out/*; do | ||
10 | wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils ]} | ||
11 | done | ||
12 | ''; | ||
13 | in | ||
14 | { | ||
15 | config.secrets.keys = [ | ||
16 | { | ||
17 | dest = "dovecot/ldap"; | ||
18 | user = config.services.dovecot2.user; | ||
19 | group = config.services.dovecot2.group; | ||
20 | permissions = "0400"; | ||
21 | text = '' | ||
22 | hosts = ${myconfig.env.mail.dovecot.ldap.host} | ||
23 | tls = yes | ||
24 | |||
25 | dn = ${myconfig.env.mail.dovecot.ldap.dn} | ||
26 | dnpass = ${myconfig.env.mail.dovecot.ldap.password} | ||
27 | |||
28 | auth_bind = yes | ||
29 | |||
30 | ldap_version = 3 | ||
31 | |||
32 | base = ${myconfig.env.mail.dovecot.ldap.base} | ||
33 | scope = subtree | ||
34 | |||
35 | user_filter = ${myconfig.env.mail.dovecot.ldap.filter} | ||
36 | pass_filter = ${myconfig.env.mail.dovecot.ldap.filter} | ||
37 | |||
38 | user_attrs = ${myconfig.env.mail.dovecot.ldap.user_attrs} | ||
39 | pass_attrs = ${myconfig.env.mail.dovecot.ldap.pass_attrs} | ||
40 | ''; | ||
41 | } | ||
42 | ]; | ||
43 | |||
44 | config.users.users.vhost = { | ||
45 | group = "vhost"; | ||
46 | uid = config.ids.uids.vhost; | ||
47 | }; | ||
48 | config.users.groups.vhost.gid = config.ids.gids.vhost; | ||
49 | |||
50 | # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous....... | ||
51 | config.services.dovecot2 = { | ||
52 | enable = true; | ||
53 | enablePAM = false; | ||
54 | enablePop3 = true; | ||
55 | enableImap = true; | ||
56 | enableLmtp = true; | ||
57 | protocols = [ "sieve" ]; | ||
58 | modules = [ | ||
59 | pkgs.dovecot_pigeonhole | ||
60 | pkgs.dovecot_deleted-to-trash | ||
61 | pkgs.dovecot_fts-xapian | ||
62 | ]; | ||
63 | mailUser = "vhost"; | ||
64 | mailGroup = "vhost"; | ||
65 | createMailUser = false; | ||
66 | mailboxes = [ | ||
67 | { name = "Trash"; auto = "subscribe"; specialUse = "Trash"; } | ||
68 | { name = "Junk"; auto = "subscribe"; specialUse = "Junk"; } | ||
69 | { name = "Sent"; auto = "subscribe"; specialUse = "Sent"; } | ||
70 | { name = "Drafts"; auto = "subscribe"; specialUse = "Drafts"; } | ||
71 | ]; | ||
72 | mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap"; | ||
73 | sslServerCert = "/var/lib/acme/mail/fullchain.pem"; | ||
74 | sslServerKey = "/var/lib/acme/mail/key.pem"; | ||
75 | sslCACert = "/var/lib/acme/mail/fullchain.pem"; | ||
76 | extraConfig = builtins.concatStringsSep "\n" [ | ||
77 | '' | ||
78 | postmaster_address = postmaster@immae.eu | ||
79 | mail_attribute_dict = file:%h/dovecot-attributes | ||
80 | imap_idle_notify_interval = 20 mins | ||
81 | namespace inbox { | ||
82 | type = private | ||
83 | separator = / | ||
84 | inbox = yes | ||
85 | list = yes | ||
86 | } | ||
87 | '' | ||
88 | |||
89 | # Full text search | ||
90 | '' | ||
91 | # needs to be bigger than any mailbox size | ||
92 | default_vsz_limit = 2GB | ||
93 | mail_plugins = $mail_plugins fts fts_xapian | ||
94 | plugin { | ||
95 | plugin = fts fts_xapian | ||
96 | fts = xapian | ||
97 | fts_xapian = partial=2 full=20 | ||
98 | fts_autoindex = yes | ||
99 | fts_autoindex_exclude = \Junk | ||
100 | fts_autoindex_exclude2 = \Trash | ||
101 | fts_autoindex_exclude3 = Virtual/* | ||
102 | } | ||
103 | '' | ||
104 | |||
105 | # Antispam | ||
106 | # https://docs.iredmail.org/dovecot.imapsieve.html | ||
107 | '' | ||
108 | # imap_sieve plugin added below | ||
109 | |||
110 | plugin { | ||
111 | sieve_plugins = sieve_imapsieve sieve_extprograms | ||
112 | imapsieve_url = sieve://127.0.0.1:4190 | ||
113 | |||
114 | # From elsewhere to Junk folder | ||
115 | imapsieve_mailbox1_name = Junk | ||
116 | imapsieve_mailbox1_causes = COPY APPEND | ||
117 | imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin | ||
118 | |||
119 | # From Junk folder to elsewhere | ||
120 | imapsieve_mailbox2_name = * | ||
121 | imapsieve_mailbox2_from = Junk | ||
122 | imapsieve_mailbox2_causes = COPY | ||
123 | imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin | ||
124 | |||
125 | sieve_pipe_bin_dir = ${sieve_bin} | ||
126 | |||
127 | sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment | ||
128 | } | ||
129 | '' | ||
130 | # Services to listen | ||
131 | '' | ||
132 | service imap-login { | ||
133 | inet_listener imap { | ||
134 | } | ||
135 | inet_listener imaps { | ||
136 | } | ||
137 | } | ||
138 | service pop3-login { | ||
139 | inet_listener pop3 { | ||
140 | } | ||
141 | inet_listener pop3s { | ||
142 | } | ||
143 | } | ||
144 | service imap { | ||
145 | } | ||
146 | service pop3 { | ||
147 | } | ||
148 | service auth { | ||
149 | unix_listener auth-userdb { | ||
150 | } | ||
151 | unix_listener ${config.services.postfix.config.queue_directory}/private/auth { | ||
152 | mode = 0666 | ||
153 | } | ||
154 | } | ||
155 | service auth-worker { | ||
156 | } | ||
157 | service dict { | ||
158 | unix_listener dict { | ||
159 | } | ||
160 | } | ||
161 | service stats { | ||
162 | unix_listener stats-reader { | ||
163 | user = vhost | ||
164 | group = vhost | ||
165 | mode = 0660 | ||
166 | } | ||
167 | unix_listener stats-writer { | ||
168 | user = vhost | ||
169 | group = vhost | ||
170 | mode = 0660 | ||
171 | } | ||
172 | } | ||
173 | '' | ||
174 | |||
175 | # Authentification | ||
176 | '' | ||
177 | first_valid_uid = ${toString config.ids.uids.vhost} | ||
178 | disable_plaintext_auth = yes | ||
179 | passdb { | ||
180 | driver = ldap | ||
181 | args = ${config.secrets.fullPaths."dovecot/ldap"} | ||
182 | } | ||
183 | userdb { | ||
184 | driver = static | ||
185 | args = user=%u uid=vhost gid=vhost home=/var/lib/vhost/%d/%n/ mail=mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap | ||
186 | } | ||
187 | '' | ||
188 | |||
189 | # Zlib | ||
190 | '' | ||
191 | mail_plugins = $mail_plugins zlib | ||
192 | plugin { | ||
193 | zlib_save_level = 6 | ||
194 | zlib_save = gz | ||
195 | } | ||
196 | '' | ||
197 | |||
198 | # Sieve | ||
199 | '' | ||
200 | plugin { | ||
201 | sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve | ||
202 | } | ||
203 | service managesieve-login { | ||
204 | } | ||
205 | service managesieve { | ||
206 | } | ||
207 | '' | ||
208 | |||
209 | # Deleted to trash | ||
210 | '' | ||
211 | plugin { | ||
212 | deleted_to_trash_folder = Trash | ||
213 | } | ||
214 | '' | ||
215 | |||
216 | # Virtual mailboxes | ||
217 | '' | ||
218 | mail_plugins = $mail_plugins virtual | ||
219 | namespace Virtual { | ||
220 | prefix = Virtual/ | ||
221 | location = virtual:~/Virtual | ||
222 | } | ||
223 | '' | ||
224 | |||
225 | # Protocol specific configuration | ||
226 | # Needs to come last if there are mail_plugins entries | ||
227 | '' | ||
228 | protocol imap { | ||
229 | mail_plugins = $mail_plugins deleted_to_trash imap_sieve | ||
230 | } | ||
231 | protocol lda { | ||
232 | mail_plugins = $mail_plugins sieve | ||
233 | } | ||
234 | '' | ||
235 | ]; | ||
236 | }; | ||
237 | config.networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ]; | ||
238 | config.system.activationScripts.dovecot = { | ||
239 | deps = [ "users" ]; | ||
240 | text ='' | ||
241 | install -m 0755 -o vhost -g vhost -d /var/lib/vhost | ||
242 | ''; | ||
243 | }; | ||
244 | |||
245 | config.security.acme.certs."mail" = { | ||
246 | postRun = '' | ||
247 | systemctl restart dovecot2.service | ||
248 | ''; | ||
249 | extraDomains = { | ||
250 | "imap.immae.eu" = null; | ||
251 | "pop3.immae.eu" = null; | ||
252 | }; | ||
253 | }; | ||
254 | } | ||
255 | |||
diff --git a/modules/private/mail.nix b/modules/private/mail/milters.nix index eb869ba..c4bd990 100644 --- a/modules/private/mail.nix +++ b/modules/private/mail/milters.nix | |||
@@ -1,16 +1,17 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | 1 | { lib, pkgs, config, myconfig, ... }: |
2 | { | 2 | { |
3 | config.users.users.nullmailer.uid = config.ids.uids.nullmailer; | 3 | options.myServices.mail.milters.sockets = lib.mkOption { |
4 | config.users.groups.nullmailer.gid = config.ids.gids.nullmailer; | 4 | type = lib.types.attrsOf lib.types.path; |
5 | 5 | default = { | |
6 | config.services.nullmailer = { | 6 | opendkim = "/run/opendkim/opendkim.sock"; |
7 | enable = true; | 7 | opendmarc = "/run/opendmarc/opendmarc.sock"; |
8 | config = { | 8 | openarc = "/run/openarc/openarc.sock"; |
9 | me = myconfig.env.mail.host; | ||
10 | remotes = "${myconfig.env.mail.relay} smtp"; | ||
11 | }; | 9 | }; |
10 | readOnly = true; | ||
11 | description = '' | ||
12 | milters sockets | ||
13 | ''; | ||
12 | }; | 14 | }; |
13 | |||
14 | config.secrets.keys = [ | 15 | config.secrets.keys = [ |
15 | { | 16 | { |
16 | dest = "opendkim/eldiron.private"; | 17 | dest = "opendkim/eldiron.private"; |
@@ -38,6 +39,7 @@ | |||
38 | config.users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ]; | 39 | config.users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ]; |
39 | config.services.opendkim = { | 40 | config.services.opendkim = { |
40 | enable = true; | 41 | enable = true; |
42 | socket = "local:${config.myServices.mail.milters.sockets.opendkim}"; | ||
41 | domains = builtins.concatStringsSep "," (lib.flatten (map | 43 | domains = builtins.concatStringsSep "," (lib.flatten (map |
42 | (zone: map | 44 | (zone: map |
43 | (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}") | 45 | (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}") |
@@ -51,6 +53,7 @@ | |||
51 | SubDomains yes | 53 | SubDomains yes |
52 | UMask 002 | 54 | UMask 002 |
53 | ''; | 55 | ''; |
56 | group = config.services.postfix.group; | ||
54 | }; | 57 | }; |
55 | config.systemd.services.opendkim.preStart = lib.mkBefore '' | 58 | config.systemd.services.opendkim.preStart = lib.mkBefore '' |
56 | # Skip the prestart script as keys are handled in secrets | 59 | # Skip the prestart script as keys are handled in secrets |
@@ -66,6 +69,7 @@ | |||
66 | config.users.users."${config.services.opendmarc.user}".extraGroups = [ "keys" ]; | 69 | config.users.users."${config.services.opendmarc.user}".extraGroups = [ "keys" ]; |
67 | config.services.opendmarc = { | 70 | config.services.opendmarc = { |
68 | enable = true; | 71 | enable = true; |
72 | socket = "local:${config.myServices.mail.milters.sockets.opendmarc}"; | ||
69 | configFile = pkgs.writeText "opendmarc.conf" '' | 73 | configFile = pkgs.writeText "opendmarc.conf" '' |
70 | AuthservID HOSTNAME | 74 | AuthservID HOSTNAME |
71 | FailureReports false | 75 | FailureReports false |
@@ -79,6 +83,7 @@ | |||
79 | TrustedAuthservIDs HOSTNAME, immae.eu, nef2.ens.fr | 83 | TrustedAuthservIDs HOSTNAME, immae.eu, nef2.ens.fr |
80 | UMask 002 | 84 | UMask 002 |
81 | ''; | 85 | ''; |
86 | group = config.services.postfix.group; | ||
82 | }; | 87 | }; |
83 | config.services.filesWatcher.opendmarc = { | 88 | config.services.filesWatcher.opendmarc = { |
84 | restart = true; | 89 | restart = true; |
@@ -90,7 +95,8 @@ | |||
90 | config.services.openarc = { | 95 | config.services.openarc = { |
91 | enable = true; | 96 | enable = true; |
92 | user = "opendkim"; | 97 | user = "opendkim"; |
93 | group = "opendkim"; | 98 | socket = "local:${config.myServices.mail.milters.sockets.openarc}"; |
99 | group = config.services.postfix.group; | ||
94 | configFile = pkgs.writeText "openarc.conf" '' | 100 | configFile = pkgs.writeText "openarc.conf" '' |
95 | AuthservID mail.immae.eu | 101 | AuthservID mail.immae.eu |
96 | Domain mail.immae.eu | 102 | Domain mail.immae.eu |
diff --git a/modules/private/mail/postfix.nix b/modules/private/mail/postfix.nix new file mode 100644 index 0000000..53bf650 --- /dev/null +++ b/modules/private/mail/postfix.nix | |||
@@ -0,0 +1,227 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | config.secrets.keys = [ | ||
4 | { | ||
5 | dest = "postfix/mysql_alias_maps"; | ||
6 | user = config.services.postfix.user; | ||
7 | group = config.services.postfix.group; | ||
8 | permissions = "0440"; | ||
9 | text = '' | ||
10 | # We need to specify that option to trigger ssl connection | ||
11 | tls_ciphers = TLSv1.2 | ||
12 | user = ${myconfig.env.mail.postfix.mysql.user} | ||
13 | password = ${myconfig.env.mail.postfix.mysql.password} | ||
14 | hosts = unix:${myconfig.env.mail.postfix.mysql.socket} | ||
15 | dbname = ${myconfig.env.mail.postfix.mysql.database} | ||
16 | query = SELECT DISTINCT destination | ||
17 | FROM forwardings_merge | ||
18 | WHERE | ||
19 | ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) | ||
20 | AND active = 1 | ||
21 | AND '%s' NOT IN | ||
22 | ( | ||
23 | SELECT source | ||
24 | FROM forwardings_blacklisted | ||
25 | WHERE source = '%s' | ||
26 | ) UNION | ||
27 | SELECT 'devnull@immae.eu' | ||
28 | FROM forwardings_blacklisted | ||
29 | WHERE source = '%s' | ||
30 | ''; | ||
31 | } | ||
32 | { | ||
33 | dest = "postfix/mysql_mailbox_maps"; | ||
34 | user = config.services.postfix.user; | ||
35 | group = config.services.postfix.group; | ||
36 | permissions = "0440"; | ||
37 | text = '' | ||
38 | # We need to specify that option to trigger ssl connection | ||
39 | tls_ciphers = TLSv1.2 | ||
40 | user = ${myconfig.env.mail.postfix.mysql.user} | ||
41 | password = ${myconfig.env.mail.postfix.mysql.password} | ||
42 | hosts = unix:${myconfig.env.mail.postfix.mysql.socket} | ||
43 | dbname = ${myconfig.env.mail.postfix.mysql.database} | ||
44 | result_format = /%d/%u | ||
45 | query = SELECT DISTINCT '%s' | ||
46 | FROM mailboxes | ||
47 | WHERE active = 1 | ||
48 | AND ( | ||
49 | (domain = '%d' AND user = '%u' AND regex = 0) | ||
50 | OR ( | ||
51 | regex = 1 | ||
52 | AND '%d' REGEXP CONCAT('^',domain,'$') | ||
53 | AND '%u' REGEXP CONCAT('^',user,'$') | ||
54 | ) | ||
55 | ) | ||
56 | LIMIT 1 | ||
57 | ''; | ||
58 | } | ||
59 | { | ||
60 | dest = "postfix/mysql_sender_login_maps"; | ||
61 | user = config.services.postfix.user; | ||
62 | group = config.services.postfix.group; | ||
63 | permissions = "0440"; | ||
64 | text = '' | ||
65 | # We need to specify that option to trigger ssl connection | ||
66 | tls_ciphers = TLSv1.2 | ||
67 | user = ${myconfig.env.mail.postfix.mysql.user} | ||
68 | password = ${myconfig.env.mail.postfix.mysql.password} | ||
69 | hosts = unix:${myconfig.env.mail.postfix.mysql.socket} | ||
70 | dbname = ${myconfig.env.mail.postfix.mysql.database} | ||
71 | query = SELECT DISTINCT destination | ||
72 | FROM forwardings_merge | ||
73 | WHERE | ||
74 | ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) | ||
75 | AND active = 1 | ||
76 | ''; | ||
77 | } | ||
78 | ]; | ||
79 | |||
80 | config.networking.firewall.allowedTCPPorts = [ 25 587 ]; | ||
81 | |||
82 | config.nixpkgs.overlays = [ (self: super: { | ||
83 | postfix = super.postfix.override { withMySQL = true; }; | ||
84 | }) ]; | ||
85 | config.users.users."${config.services.postfix.user}".extraGroups = [ "keys" ]; | ||
86 | config.services.filesWatcher.postfix = { | ||
87 | restart = true; | ||
88 | paths = [ | ||
89 | config.secrets.fullPaths."postfix/mysql_alias_maps" | ||
90 | config.secrets.fullPaths."postfix/mysql_mailbox_maps" | ||
91 | config.secrets.fullPaths."postfix/mysql_sender_login_maps" | ||
92 | ]; | ||
93 | }; | ||
94 | config.services.postfix = { | ||
95 | mapFiles = let | ||
96 | name = n: i: "relay_${n}_${toString i}"; | ||
97 | pair = n: i: m: lib.attrsets.nameValuePair (name n i) ( | ||
98 | if m.type == "hash" | ||
99 | then pkgs.writeText (name n i) m.content | ||
100 | else null | ||
101 | ); | ||
102 | pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps; | ||
103 | in | ||
104 | lib.attrsets.filterAttrs (k: v: v != null) ( | ||
105 | lib.attrsets.listToAttrs (lib.flatten ( | ||
106 | lib.attrsets.mapAttrsToList pairs myconfig.env.mail.postfix.backup_domains | ||
107 | )) | ||
108 | ); | ||
109 | config = { | ||
110 | ### postfix module overrides | ||
111 | readme_directory = "${pkgs.postfix}/share/postfix/doc"; | ||
112 | smtp_tls_CAfile = lib.mkForce ""; | ||
113 | smtp_tls_cert_file = lib.mkForce ""; | ||
114 | smtp_tls_key_file = lib.mkForce ""; | ||
115 | |||
116 | message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited" | ||
117 | alias_database = "\$alias_maps"; | ||
118 | |||
119 | ### Virtual mailboxes config | ||
120 | virtual_alias_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"; | ||
121 | virtual_mailbox_domains = myconfig.env.mail.postfix.additional_mailbox_domains | ||
122 | ++ lib.remove "localhost.immae.eu" (lib.remove null (lib.flatten (map | ||
123 | (zone: map | ||
124 | (e: if e.receive | ||
125 | then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}" | ||
126 | else null | ||
127 | ) | ||
128 | (zone.withEmail or []) | ||
129 | ) | ||
130 | myconfig.env.dns.masterZones | ||
131 | ))); | ||
132 | virtual_mailbox_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_mailbox_maps"}"; | ||
133 | dovecot_destination_recipient_limit = "1"; | ||
134 | virtual_transport = "dovecot"; | ||
135 | |||
136 | ### Relay domains | ||
137 | relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) myconfig.env.mail.postfix.backup_domains); | ||
138 | relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v: | ||
139 | lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps | ||
140 | ) myconfig.env.mail.postfix.backup_domains); | ||
141 | |||
142 | ### Additional smtpd configuration | ||
143 | smtpd_tls_received_header = "yes"; | ||
144 | smtpd_tls_loglevel = "1"; | ||
145 | |||
146 | ### Email sending configuration | ||
147 | smtp_tls_security_level = "may"; | ||
148 | smtp_tls_loglevel = "1"; | ||
149 | |||
150 | ### Force ip bind for smtp | ||
151 | smtp_bind_address = myconfig.env.servers.eldiron.ips.main.ip4; | ||
152 | smtp_bind_address6 = builtins.head myconfig.env.servers.eldiron.ips.main.ip6; | ||
153 | |||
154 | # #Unneeded if postfix can only send e-mail from "self" domains | ||
155 | # #smtp_sasl_auth_enable = "yes"; | ||
156 | # #smtp_sasl_password_maps = "hash:/etc/postfix/relay_creds"; | ||
157 | # #smtp_sasl_security_options = "noanonymous"; | ||
158 | # #smtp_sender_dependent_authentication = "yes"; | ||
159 | # #sender_dependent_relayhost_maps = "hash:/etc/postfix/sender_relay"; | ||
160 | |||
161 | ### opendkim, opendmarc, openarc milters | ||
162 | non_smtpd_milters = [ | ||
163 | "unix:${config.myServices.mail.milters.sockets.opendkim}" | ||
164 | "unix:${config.myServices.mail.milters.sockets.opendmarc}" | ||
165 | "unix:${config.myServices.mail.milters.sockets.openarc}" | ||
166 | ]; | ||
167 | smtpd_milters = [ | ||
168 | "unix:${config.myServices.mail.milters.sockets.opendkim}" | ||
169 | "unix:${config.myServices.mail.milters.sockets.opendmarc}" | ||
170 | "unix:${config.myServices.mail.milters.sockets.openarc}" | ||
171 | ]; | ||
172 | }; | ||
173 | enable = true; | ||
174 | enableSmtp = true; | ||
175 | enableSubmission = true; | ||
176 | submissionOptions = { | ||
177 | smtpd_tls_security_level = "encrypt"; | ||
178 | smtpd_sasl_auth_enable = "yes"; | ||
179 | smtpd_tls_auth_only = "yes"; | ||
180 | smtpd_sasl_tls_security_options = "noanonymous"; | ||
181 | smtpd_sasl_type = "dovecot"; | ||
182 | smtpd_sasl_path = "private/auth"; | ||
183 | smtpd_reject_unlisted_recipient = "no"; | ||
184 | smtpd_client_restrictions = "permit_sasl_authenticated,reject"; | ||
185 | # Refuse to send e-mails with a From that is not handled | ||
186 | smtpd_sender_restrictions = | ||
187 | "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject"; | ||
188 | smtpd_sender_login_maps = "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}"; | ||
189 | smtpd_recipient_restrictions = "permit_sasl_authenticated,reject"; | ||
190 | milter_macro_daemon_name = "ORIGINATING"; | ||
191 | smtpd_milters = "unix:${config.myServices.mail.milters.sockets.opendkim}"; | ||
192 | }; | ||
193 | destination = ["localhost"]; | ||
194 | # This needs to reverse DNS | ||
195 | hostname = "eldiron.immae.eu"; | ||
196 | setSendmail = true; | ||
197 | sslCert = "/var/lib/acme/mail/fullchain.pem"; | ||
198 | sslKey = "/var/lib/acme/mail/key.pem"; | ||
199 | recipientDelimiter = "+"; | ||
200 | masterConfig = { | ||
201 | dovecot = { | ||
202 | type = "unix"; | ||
203 | privileged = true; | ||
204 | chroot = false; | ||
205 | command = "pipe"; | ||
206 | args = let | ||
207 | # rspamd could be used as a milter, but then it cannot apply | ||
208 | # its checks "per user" (milter is not yet dispatched to | ||
209 | # users), so we wrap dovecot-lda inside rspamc per recipient | ||
210 | # here. | ||
211 | dovecot_exe = "${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f \${sender} -a \${original_recipient} -d \${user}@\${nexthop}"; | ||
212 | in [ | ||
213 | "flags=DRhu" "user=vhost:vhost" | ||
214 | "argv=${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d \${user}@\${nexthop} --mime --exec {${dovecot_exe}}" | ||
215 | ]; | ||
216 | }; | ||
217 | }; | ||
218 | }; | ||
219 | config.security.acme.certs."mail" = { | ||
220 | postRun = '' | ||
221 | systemctl restart postfix.service | ||
222 | ''; | ||
223 | extraDomains = { | ||
224 | "smtp.immae.eu" = null; | ||
225 | }; | ||
226 | }; | ||
227 | } | ||
diff --git a/modules/private/mail/rspamd.nix b/modules/private/mail/rspamd.nix new file mode 100644 index 0000000..3a7a67c --- /dev/null +++ b/modules/private/mail/rspamd.nix | |||
@@ -0,0 +1,84 @@ | |||
1 | { lib, pkgs, config, myconfig, ... }: | ||
2 | { | ||
3 | options.myServices.mail.rspamd.sockets = lib.mkOption { | ||
4 | type = lib.types.attrsOf lib.types.path; | ||
5 | default = { | ||
6 | worker-controller = "/run/rspamd/worker-controller.sock"; | ||
7 | }; | ||
8 | readOnly = true; | ||
9 | description = '' | ||
10 | rspamd sockets | ||
11 | ''; | ||
12 | }; | ||
13 | config.services.cron.systemCronJobs = let | ||
14 | cron_script = pkgs.runCommand "cron_script" { | ||
15 | buildInputs = [ pkgs.makeWrapper ]; | ||
16 | } '' | ||
17 | mkdir -p $out | ||
18 | cp ${./scan_reported_mails} $out/scan_reported_mails | ||
19 | patchShebangs $out | ||
20 | for i in $out/*; do | ||
21 | wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.rspamd pkgs.flock ]} | ||
22 | done | ||
23 | ''; | ||
24 | in | ||
25 | [ "*/20 * * * * vhost ${cron_script}/scan_reported_mails" ]; | ||
26 | |||
27 | config.services.rspamd = { | ||
28 | enable = true; | ||
29 | debug = true; | ||
30 | overrides = { | ||
31 | "actions.conf".text = '' | ||
32 | reject = null; | ||
33 | add_header = 6; | ||
34 | greylist = null; | ||
35 | ''; | ||
36 | "milter_headers.conf".text = '' | ||
37 | extended_spam_headers = true; | ||
38 | ''; | ||
39 | }; | ||
40 | locals = { | ||
41 | "redis.conf".text = '' | ||
42 | servers = "${myconfig.env.mail.rspamd.redis.socket}"; | ||
43 | db = "${myconfig.env.mail.rspamd.redis.db}"; | ||
44 | ''; | ||
45 | "classifier-bayes.conf".text = '' | ||
46 | users_enabled = true; | ||
47 | backend = "redis"; | ||
48 | servers = "${myconfig.env.mail.rspamd.redis.socket}"; | ||
49 | database = "${myconfig.env.mail.rspamd.redis.db}"; | ||
50 | autolearn = true; | ||
51 | cache { | ||
52 | backend = "redis"; | ||
53 | } | ||
54 | new_schema = true; | ||
55 | statfile { | ||
56 | BAYES_HAM { | ||
57 | spam = false; | ||
58 | } | ||
59 | BAYES_SPAM { | ||
60 | spam = true; | ||
61 | } | ||
62 | } | ||
63 | ''; | ||
64 | }; | ||
65 | workers = { | ||
66 | controller = { | ||
67 | extraConfig = '' | ||
68 | enable_password = "${myconfig.env.mail.rspamd.write_password_hashed}"; | ||
69 | password = "${myconfig.env.mail.rspamd.read_password_hashed}"; | ||
70 | ''; | ||
71 | bindSockets = [ { | ||
72 | socket = config.myServices.mail.rspamd.sockets.worker-controller; | ||
73 | mode = "0660"; | ||
74 | owner = config.services.rspamd.user; | ||
75 | group = "vhost"; | ||
76 | } ]; | ||
77 | }; | ||
78 | }; | ||
79 | postfix = { | ||
80 | enable = true; | ||
81 | config = {}; | ||
82 | }; | ||
83 | }; | ||
84 | } | ||
diff --git a/modules/private/mail/scan_reported_mails b/modules/private/mail/scan_reported_mails new file mode 100755 index 0000000..fe9f4d6 --- /dev/null +++ b/modules/private/mail/scan_reported_mails | |||
@@ -0,0 +1,21 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | |||
3 | ( flock -n 9 || exit 1 | ||
4 | shopt -s nullglob | ||
5 | for spool in /var/lib/vhost/.rspamd/*/pending; do | ||
6 | rspamd_folder=$(dirname $spool) | ||
7 | mail_user=$(basename $rspamd_folder) | ||
8 | mv $rspamd_folder/pending $rspamd_folder/processing | ||
9 | |||
10 | for mtype in ham spam; do | ||
11 | if [ -d $rspamd_folder/processing/$mtype ]; then | ||
12 | output="$(rspamc -h /run/rspamd/worker-controller.sock -c bayes -d $mail_user learn_$mtype $rspamd_folder/processing/$mtype/*)" | ||
13 | echo "[$mtype: $mail_user]" ${output} >> /var/lib/vhost/.rspamd/rspamd.log | ||
14 | mkdir -p $rspamd_folder/processed/$mtype | ||
15 | cp $rspamd_folder/processing/$mtype/* $rspamd_folder/processed/$mtype/ | ||
16 | fi | ||
17 | done | ||
18 | |||
19 | rm -rf $rspamd_folder/processing | ||
20 | done | ||
21 | ) 9>/var/lib/vhost/scan_reported_mails.lock | ||
diff --git a/modules/private/mail/sieve_bin/imapsieve_copy b/modules/private/mail/sieve_bin/imapsieve_copy new file mode 100755 index 0000000..2ca1f23 --- /dev/null +++ b/modules/private/mail/sieve_bin/imapsieve_copy | |||
@@ -0,0 +1,8 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | # Inspired from https://docs.iredmail.org/dovecot.imapsieve.html | ||
3 | |||
4 | MSG_TYPE="$1" | ||
5 | OUTPUT_DIR="/var/lib/vhost/.rspamd/${USER}/pending/${MSG_TYPE}" | ||
6 | FILE="${OUTPUT_DIR}/$(date +%Y%m%d%H%M%S)-${RANDOM}${RANDOM}.eml" | ||
7 | mkdir -p "${OUTPUT_DIR}" | ||
8 | cat > ${FILE} < /dev/stdin | ||
diff --git a/modules/private/mail/sieve_scripts/report_ham.sieve b/modules/private/mail/sieve_scripts/report_ham.sieve new file mode 100644 index 0000000..f9b8481 --- /dev/null +++ b/modules/private/mail/sieve_scripts/report_ham.sieve | |||
@@ -0,0 +1,11 @@ | |||
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; | ||
2 | |||
3 | if environment :matches "imap.mailbox" "*" { | ||
4 | set "mailbox" "${1}"; | ||
5 | } | ||
6 | |||
7 | if string "${mailbox}" "Trash" { | ||
8 | stop; | ||
9 | } | ||
10 | |||
11 | pipe :copy "imapsieve_copy" [ "ham" ]; | ||
diff --git a/modules/private/mail/sieve_scripts/report_spam.sieve b/modules/private/mail/sieve_scripts/report_spam.sieve new file mode 100644 index 0000000..9a1f794 --- /dev/null +++ b/modules/private/mail/sieve_scripts/report_spam.sieve | |||
@@ -0,0 +1,3 @@ | |||
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve" ]; | ||
2 | |||
3 | pipe :copy "imapsieve_copy" [ "spam" ]; | ||
diff --git a/modules/private/websites/tools/tools/roundcubemail.nix b/modules/private/websites/tools/tools/roundcubemail.nix index 6d87cdc..8bb60d6 100644 --- a/modules/private/websites/tools/tools/roundcubemail.nix +++ b/modules/private/websites/tools/tools/roundcubemail.nix | |||
@@ -17,7 +17,6 @@ rec { | |||
17 | text = '' | 17 | text = '' |
18 | <?php | 18 | <?php |
19 | $config['db_dsnw'] = '${env.psql_url}'; | 19 | $config['db_dsnw'] = '${env.psql_url}'; |
20 | // This is used as default @domain, don't use "imap.immae.eu" here! | ||
21 | $config['default_host'] = 'ssl://imap.immae.eu'; | 20 | $config['default_host'] = 'ssl://imap.immae.eu'; |
22 | $config['username_domain'] = array( | 21 | $config['username_domain'] = array( |
23 | "imap.immae.eu" => "mail.immae.eu" | 22 | "imap.immae.eu" => "mail.immae.eu" |
@@ -46,6 +45,7 @@ rec { | |||
46 | 'identicon', | 45 | 'identicon', |
47 | 'identity_select', | 46 | 'identity_select', |
48 | 'jqueryui', | 47 | 'jqueryui', |
48 | 'markasjunk', | ||
49 | 'managesieve', | 49 | 'managesieve', |
50 | 'newmail_notifier', | 50 | 'newmail_notifier', |
51 | 'vcard_attachments', | 51 | 'vcard_attachments', |
@@ -60,11 +60,11 @@ rec { | |||
60 | 60 | ||
61 | $config['language'] = 'fr_FR'; | 61 | $config['language'] = 'fr_FR'; |
62 | 62 | ||
63 | $config['drafts_mbox'] = 'Mail/Drafts'; | 63 | $config['drafts_mbox'] = 'Drafts'; |
64 | $config['junk_mbox'] = 'Mail/Spam'; | 64 | $config['junk_mbox'] = 'Junk'; |
65 | $config['sent_mbox'] = 'Mail/sent'; | 65 | $config['sent_mbox'] = 'Sent'; |
66 | $config['trash_mbox'] = '''; | 66 | $config['trash_mbox'] = 'Trash'; |
67 | $config['default_folders'] = array('INBOX', 'Mail/Drafts', 'Mail/sent', 'Mail/Spam', '''); | 67 | $config['default_folders'] = array('INBOX', 'Drafts', 'Sent', 'Junk', 'Trash'); |
68 | $config['draft_autosave'] = 60; | 68 | $config['draft_autosave'] = 60; |
69 | $config['enable_installer'] = false; | 69 | $config['enable_installer'] = false; |
70 | $config['log_driver'] = 'file'; | 70 | $config['log_driver'] = 'file'; |
diff --git a/pkgs/default.nix b/pkgs/default.nix index 74f9d18..ff9d477 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix | |||
@@ -50,4 +50,10 @@ rec { | |||
50 | python = python3; | 50 | python = python3; |
51 | inherit mylibs; | 51 | inherit mylibs; |
52 | }; | 52 | }; |
53 | dovecot_deleted-to-trash = callPackage ./dovecot/plugins/deleted_to_trash { | ||
54 | inherit mylibs; | ||
55 | }; | ||
56 | dovecot_fts-xapian = callPackage ./dovecot/plugins/fts_xapian { | ||
57 | inherit mylibs; | ||
58 | }; | ||
53 | } | 59 | } |
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/default.nix b/pkgs/dovecot/plugins/deleted_to_trash/default.nix new file mode 100644 index 0000000..db1afb5 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/default.nix | |||
@@ -0,0 +1,21 @@ | |||
1 | { stdenv, fetchurl, dovecot, mylibs, fetchpatch }: | ||
2 | |||
3 | stdenv.mkDerivation (mylibs.fetchedGithub ./dovecot-deleted_to_trash.json // rec { | ||
4 | buildInputs = [ dovecot ]; | ||
5 | patches = [ | ||
6 | (fetchpatch { | ||
7 | name = "fix-dovecot-2.3.diff"; | ||
8 | url = "https://github.com/lexbrugman/dovecot_deleted_to_trash/commit/c52a3799a96104a603ade33404ef6aa1db647b2f.diff"; | ||
9 | sha256 = "0pld3rdcjp9df2qxbp807k6v4f48lyk0xy5q508ypa57d559y6dq"; | ||
10 | }) | ||
11 | ./fix_mbox.patch | ||
12 | ]; | ||
13 | preConfigure = '' | ||
14 | substituteInPlace Makefile --replace \ | ||
15 | "/usr/include/dovecot" \ | ||
16 | "${dovecot}/include/dovecot" | ||
17 | substituteInPlace Makefile --replace \ | ||
18 | "/usr/lib/dovecot/modules" \ | ||
19 | "$out/lib/dovecot" | ||
20 | ''; | ||
21 | }) | ||
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json b/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json new file mode 100644 index 0000000..2987a02 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/dovecot-deleted_to_trash.json | |||
@@ -0,0 +1,15 @@ | |||
1 | { | ||
2 | "tag": "81b0754-master", | ||
3 | "meta": { | ||
4 | "name": "dovecot-deleted_to_trash", | ||
5 | "url": "https://github.com/lexbrugman/dovecot_deleted_to_trash", | ||
6 | "branch": "master" | ||
7 | }, | ||
8 | "github": { | ||
9 | "owner": "lexbrugman", | ||
10 | "repo": "dovecot_deleted_to_trash", | ||
11 | "rev": "81b07549accfc36467bf8527a53c295c7a02dbb9", | ||
12 | "sha256": "1b3k31g898s4fa0a9l4kvjsdyds772waaay84sjdxv09jw6mqs0f", | ||
13 | "fetchSubmodules": true | ||
14 | } | ||
15 | } | ||
diff --git a/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch b/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch new file mode 100644 index 0000000..0060fb4 --- /dev/null +++ b/pkgs/dovecot/plugins/deleted_to_trash/fix_mbox.patch | |||
@@ -0,0 +1,12 @@ | |||
1 | diff --git a/src/deleted-to-trash-plugin.c b/src/deleted-to-trash-plugin.c | ||
2 | index bb4cc78..66bad53 100644 | ||
3 | --- a/src/deleted-to-trash-plugin.c | ||
4 | +++ b/src/deleted-to-trash-plugin.c | ||
5 | @@ -82,6 +82,7 @@ static struct mailbox *mailbox_open_or_create(struct mailbox_list *list, const c | ||
6 | *error_r = mail_storage_get_last_error(mailbox_get_storage(box), &error); | ||
7 | if (error != MAIL_ERROR_NOTFOUND) | ||
8 | { | ||
9 | + i_error("%s", *error_r); | ||
10 | mailbox_free(&box); | ||
11 | return NULL; | ||
12 | } | ||
diff --git a/pkgs/dovecot/plugins/fts_xapian/default.nix b/pkgs/dovecot/plugins/fts_xapian/default.nix new file mode 100644 index 0000000..350a3ff --- /dev/null +++ b/pkgs/dovecot/plugins/fts_xapian/default.nix | |||
@@ -0,0 +1,14 @@ | |||
1 | { stdenv, autoconf, automake, pkg-config, dovecot, libtool, xapian, icu, mylibs }: | ||
2 | |||
3 | stdenv.mkDerivation (mylibs.fetchedGithub ./fts-xapian.json // rec { | ||
4 | buildInputs = [ dovecot autoconf automake libtool pkg-config xapian icu ]; | ||
5 | preConfigure = '' | ||
6 | export PANDOC=false | ||
7 | autoreconf -vi | ||
8 | ''; | ||
9 | configureFlags = [ | ||
10 | "--with-dovecot=${dovecot}/lib/dovecot" | ||
11 | "--without-dovecot-install-dirs" | ||
12 | "--with-moduledir=$(out)/lib/dovecot" | ||
13 | ]; | ||
14 | }) | ||
diff --git a/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json b/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json new file mode 100644 index 0000000..a786776 --- /dev/null +++ b/pkgs/dovecot/plugins/fts_xapian/fts-xapian.json | |||
@@ -0,0 +1,15 @@ | |||
1 | { | ||
2 | "tag": "9a94b4a-master", | ||
3 | "meta": { | ||
4 | "name": "fts-xapian", | ||
5 | "url": "https://github.com/grosjo/fts-xapian", | ||
6 | "branch": "master" | ||
7 | }, | ||
8 | "github": { | ||
9 | "owner": "grosjo", | ||
10 | "repo": "fts-xapian", | ||
11 | "rev": "9a94b4aeaac3988786ad72a716127c306b05c9d6", | ||
12 | "sha256": "12xv5fnqahs0cy26ja2jwk6dg95626amblisf2wcx3nqzkcf4w1y", | ||
13 | "fetchSubmodules": true | ||
14 | } | ||
15 | } | ||