diff --git a/group_vars/mailman.yml b/group_vars/mailman.yml
index fe7a0de7484a5cce7f0f355cf5f4c52d45b29b65..aa2eef5a2009b439b7414ec1e6e5c7f1c063e812 100644
--- a/group_vars/mailman.yml
+++ b/group_vars/mailman.yml
@@ -1,61 +1,84 @@
 ---
 loc_nginx:
-  service_name: mailman
-  default_server: lists.crans.org
-  default_ssl_server: lists.crans.org
-  auth_passwd:
-    Stop: "$apr1$NXaV5H7Q$J3ora3Jo5h775Y1nm93PN1"
-  deploy_robots_file: true
+  service_name: mailman3
+  upstreams:
+    - name: mailman3
+      server: "unix:/run/mailman3-web/uwsgi.sock fail_timeout=0"
   servers:
-    - server_name:
-      - lists.crans.org
-      ssl: crans.org
-      root: "/usr/lib/cgi-bin/mailman/"
-      index:
-        - index.htm
-        - index.html
+    - ssl: false
+      server_name:
+        - "localhost"
       locations:
-        - filter: "/error/"
-          params:
-            - "internal"
-            - "alias /var/www/html/"
-        - filter: "/create"
-          params:
-            - "default_type text/html"
-            - "alias /etc/mailman/create.html"
-        - filter: "~ ^/$"
-          params:
-            - "return 302 https://lists.crans.org/listinfo"
         - filter: "/"
           params:
-            - "include \"/etc/nginx/snippets/fastcgi-mailman.conf\""
-        - filter: "~ ^/listinfo"
+            - "uwsgi_pass mailman3"
+            - "include /etc/nginx/uwsgi_params"
+
+    - ssl: false
+      default: true
+      server_name:
+        - "lists.crans.org"
+      locations:
+        - filter: "/"
           params:
+            - "uwsgi_pass mailman3"
+            - "include /etc/nginx/uwsgi_params"
             - "satisfy any"
-            - "include \"/etc/nginx/snippets/fastcgi-mailman.conf\""
             - "allow 185.230.76.0/22"
             - "allow 2a0c:700:0::/40"
             - "deny all"
             - "auth_basic \"On n'aime pas les spambots, donc on a mis un mot de passe. Le login est Stop et le mot de passe est Spam.\""
             - "auth_basic_user_file /etc/nginx/passwd"
             - "error_page 401 /error/401.html"
-        - filter: "~ ^/admin"
+
+        - filter: "/mailman3/static"
           params:
-            - "satisfy any"
-            - "include \"/etc/nginx/snippets/fastcgi-mailman.conf\""
-            - "allow 185.230.76.0/22"
-            - "allow 2a0c:700:0::/40"
-            - "deny all"
-            - "auth_basic \"On n'aime pas les spambots, donc on a mis un mot de passe. Le login est Stop et le mot de passe est Spam.\""
-            - "auth_basic_user_file /etc/nginx/passwd"
-            - "error_page 401 /error/401.html"
-        - filter: "/images/mailman"
+            - "alias /var/lib/mailman3/web/static"
+
+        - filter: "/mailman3/static/favicon.ico"
+          params:
+            - "alias /var/lib/mailman3/web/static/postorius/img/favicon.ico"
+
+        - filter: "/error/"
           params:
-            - "alias /usr/share/images/mailman"
+            - "internal"
+            - "alias /var/www/html/"
+
         - filter: "/robots.txt"
           params:
             - "alias /var/www/robots.txt"
-        - filter: "/archives"
-          params:
-            - "alias /var/lib/mailman/archives/public"
-            - "autoindex on"
+
+  auth_passwd:
+    Stop: "$apr1$NXaV5H7Q$J3ora3Jo5h775Y1nm93PN1"  # Spam
+  deploy_robots_file: true
+
+glob_mailman3:
+  site_owner: root@crans.org
+  database:
+    user: "mailman3"
+    pass: "{{ vault.mailman3_database_pass }}"
+    host: "{{ query('ldap', 'ip', 'tealc', 'adm') | ipv4 | first }}"
+    port: 5432
+    name: "mailman3"
+  web_database:
+    user: "mailman3web"
+    pass: "{{ vault.mailman3_web_database_pass }}"
+    host: "{{ query('ldap', 'ip', 'tealc', 'adm') | ipv4 | first }}"
+    port: 5432
+    name: "mailman3web"
+  restadmin_pass: "{{ vault.mailman3_restadmin_pass }}"
+  archiver_key: "{{ vault.mailman3_archiver_key }}"
+  web_secret_key: "{{ vault.mailman3_web_secret_key }}"
+  web_domains:
+    - "lists.crans.org"
+  default_domain: "crans.org"
+  postfix_domain: "crans.org"
+
+loc_opendkim:
+  domain: "lists.crans.org"
+  selector: "lists"
+  signing:
+    - "*@lists.crans.org"
+  txt_record: |
+    lists._domainkey IN TXT "v=DKIM1; h=sha256; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7jkgGjxZvQDbgFIuqb59lt7O1Jg6DFTSBxFlTfBW+3MF+AFjBR3AZ/UXwDA1vH4UTZqq0fWN6y6wqE/F7+HDjpqZGGuygZWTGVbBxwiKSjc2kq2mz7kLisE3a/jP8kyQDdb7fWrtTw9fxYu+Ygs0744otjRsui/ZK6zbrO8XQfd5UYnj4IGALeIuVFVLmwTY+VL/xWR/UjcfxgAprRfH0ec8PGlrxhpeLhUSJxw3Q6QfTnDsIpWLfJdgxILGa58TmhH6d+faxa1OIP37wswPjkDykmMFsCQJX9P7mXXR0+1FIRhhNpfCRXXj37udbIezDEMfA15rWSoYinPU+x7i6LhfJD7G40p1oDBiaOimZ8D/PUDAtoWRQeFiNOOQmNqDaVwlaOMvIZH2ZFD2I0eJIDb2FBYqhTb5GVyhKPePqT5FZE0s8SXqvYRNUWHuomS79kfo4TC74UPlavIbyCVTFlLi5ujc5RANm/FuH2w3ns1+YAlCeoblzwVdgN+h4/DI5kI88+0Hf+HCfQg+rPQL7ak7Wszo0iWvYUZ8t+IPbNDcVm5YI6koqkWGgfMrC0bDI5r+ZQACK15Fi6x3tV0umhytgRQWG9MyK61diNIc1LFsyL2lD0oOAjlpDlVSpUnXKhjRPq4YdaIojlgGSsWsq8sBhQTCY5DNHUuJLL1iPqsCAwEAAQ=="  ; ----- DKIM key lists for lists.crans.org
+  private_key: "{{ vault.opendkim_private_key_mailman }}"
diff --git a/group_vars/opendkim.yml b/group_vars/opendkim.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d69a6b5d680f5b598790ca87ad4f67b2e02585f3
--- /dev/null
+++ b/group_vars/opendkim.yml
@@ -0,0 +1,21 @@
+---
+glob_opendkim:
+  domain: "crans.org"
+  selector: "mail"
+  signing:
+    - "*@crans.org"
+    - "*@crans.fr"
+    - "*@crans.eu"
+  trust:
+    - "185.230.79.0/26"
+    - "172.16.3.0/24"
+    - "172.16.10.0/24"
+    - "2a0c:700:0:2::/64"
+    - "2a0c:700:0:3::/64"
+    - "2a0c:700:0:10::/64"
+    - "*@crans.org"
+    - "*@crans.fr"
+    - "*@crans.eu"
+  txt_record: |
+    mail._domainkey IN TXT "v=DKIM1; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtwkNVd9Mmz8S4WcfuPk0X2drG39gS8+uxAv8igRILgzWeN8j2hjeZesl8pm/1UTVU87bYcdfUgXiGfQy9nR5p/Vmt2kS7sXk9nsJ/VYENgb3IJQ6paWupSTFMyeKycJ4ZHCEZB/bVvifoG6vLKqW5jpsfCiOcfdcgXATn0UPuVx9t93yRrhoEMntMv9TSodjqd3FKCtJUoh5cNQHo0T6dWKtxoIgNi/mvZ92D/IACwu/XOU+Rq9fnoEI8GukBQUR5AkP0B/JrvwWXWX/3EjY8X37ljEX0XUdq/ShzTl5iK+CM83stgkFUQh/rpww5mnxYEW3X4uirJ7VJHmY4KPoIU+2DPjLQj9Hz63CMWY3Ks2pXWzxD3V+GI1aJTMFOv2LeHnI3ScqFaKj9FR4ZKMb0OW2BEFBIY3J3aeo/paRwdbVCMM7twDtZY9uInR/NhVa1v9hlOxwp4/2pGSKQYoN2CkAZ1Alzwf8M3EONLKeiC43JLYwKH1uBB1oikSVhMnLjG0219XvfG/tphyoOqJR/bCc2rdv5pLwKUl4wVuygfpvOw12bcvnTfYuk/BXzVHg9t4H8k/DJR6GAoeNAapXIS8AfAScF8QdKfplhKLJyQGJ6lQ75YD9IwRAN0oV+8NTjl46lI/C+b7mpfXCew+p6YPwfNvV2shiR0Ez8ZGUQIcCAwEAAQ==" ; ----- DKIM key mail for crans.org
+  private_key: "{{ vault.opendkim_private_key }}"
diff --git a/group_vars/reverseproxy.yml b/group_vars/reverseproxy.yml
index 734bc323b21a16f606083679129282dda417d508..cbb73a0ccc18d34cd840b863c487ccfa83b0b292 100644
--- a/group_vars/reverseproxy.yml
+++ b/group_vars/reverseproxy.yml
@@ -43,7 +43,7 @@ glob_reverseproxy:
     - {from: owncloud.crans.org, to: 172.16.10.136}
     - {from: linx.crans.org, to: "172.16.10.119:8080"}
     - {from: belenios.crans.org, to: 172.16.10.111}
-    # - {from: mailman.crans.org, to: 10.231.136.180}
+    - {from: lists.crans.org, to: 172.16.10.110}
 
     # Zamok
     - {from: perso.crans.org, to: 172.16.10.31}
diff --git a/host_vars/boeing.adm.crans.org.yml b/host_vars/boeing.adm.crans.org.yml
index fe9d1c69afc8d9e28107cc3fdf127b8f6071009d..882cb80deca805f98346ed49eb3f96592ccd35dd 100644
--- a/host_vars/boeing.adm.crans.org.yml
+++ b/host_vars/boeing.adm.crans.org.yml
@@ -7,5 +7,4 @@ postfix:
   secondary: true
   public: true
   dkim: true
-  mailman: false
   titanic: true
diff --git a/host_vars/mailman.adm.crans.org.yml b/host_vars/mailman.adm.crans.org.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c6f8791bd46135e7d141c9466987a2ddfba81adf
--- /dev/null
+++ b/host_vars/mailman.adm.crans.org.yml
@@ -0,0 +1,4 @@
+---
+interfaces:
+  adm: eth0
+  srv: eth1
diff --git a/host_vars/redisdead.adm.crans.org.yml b/host_vars/redisdead.adm.crans.org.yml
index 8228a1d0f68dc4a674859cef562009440f788244..999c2eb4d7632226889c96cd6d20d9a7bf55aef0 100644
--- a/host_vars/redisdead.adm.crans.org.yml
+++ b/host_vars/redisdead.adm.crans.org.yml
@@ -8,7 +8,6 @@ postfix:
   secondary: false
   public: true
   dkim: true
-  mailman: true
   titanic: false
 
 to_backup:
diff --git a/host_vars/sputnik.adm.crans.org.yml b/host_vars/sputnik.adm.crans.org.yml
index 7e6ff41c5707a678975880815c188a73962074d5..0ad18335eb30ae0a5a56f2a413d50188b85c5386 100644
--- a/host_vars/sputnik.adm.crans.org.yml
+++ b/host_vars/sputnik.adm.crans.org.yml
@@ -4,7 +4,6 @@ postfix:
   secondary: true
   public: true
   dkim: true
-  mailman: false
   titanic: false
 
 to_backup:
diff --git a/hosts b/hosts
index 0377e0431e53bbd7097c0bca97cdc5b841bfd4f6..e191f476e4a05bf2ac48fe11c2ee243884f18061 100644
--- a/hosts
+++ b/hosts
@@ -79,18 +79,11 @@ jitsi.adm.crans.org
 [keepalived:children]
 routeurs_vm
 
-[slapd]
-tealc.adm.crans.org
-sam.adm.crans.org
-daniel.adm.crans.org
-jack.adm.crans.org
-sputnik.adm.crans.org
-
 [linx]
 linx.adm.crans.org
 
 [mailman]
-redisdead.adm.crans.org
+mailman.adm.crans.org
 
 [monitoring]
 monitoring.adm.crans.org
@@ -111,13 +104,17 @@ wiki
 charybde.adm.crans.org
 # silice.adm.crans.org
 
+[opendkim:children]
+mailman
+postfix
+
 [postfix]
-mailman.adm.crans.org
 redisdead.adm.crans.org
 zamok.adm.crans.org
 
 [postfix:children]
 freebox
+mailman
 ovh_physical
 
 [radius:children]
@@ -144,6 +141,13 @@ routeur-daniel.adm.crans.org
 routeur-jack.adm.crans.org
 routeur-sam.adm.crans.org
 
+[slapd]
+tealc.adm.crans.org
+sam.adm.crans.org
+daniel.adm.crans.org
+jack.adm.crans.org
+sputnik.adm.crans.org
+
 [thelounge]
 irc.adm.crans.org
 zamok.adm.crans.org
@@ -191,6 +195,7 @@ kenobi.adm.crans.org
 kiwi.adm.crans.org
 kiwijuice.adm.crans.org
 linx.adm.crans.org
+mailman.adm.crans.org
 monitoring.adm.crans.org
 owl.adm.crans.org
 owncloud.adm.crans.org
diff --git a/plays/mailman.yml b/plays/mailman.yml
index ac7afd009a7cb6b418ec1aa45473d204f2d99870..cd80ad80ccf2e953a68f778888cbbf6b465c6971 100755
--- a/plays/mailman.yml
+++ b/plays/mailman.yml
@@ -1,36 +1,15 @@
 #!/usr/bin/env ansible-playbook
 ---
-# Deploy Mailman
-- hosts: redisdead.adm.crans.org
+# Deploy Mailman3
+- hosts: mailman
   vars:
-    mailman:
-      site_list: "nounou"
-      default_url: "https://lists.crans.org/"
-      default_host: "lists.crans.org"
-      default_language: "fr"
-      custom_logo: "crans_icon_dark.svg"
-      custom_logo_name: "crans.svg"
-      custom_logo_url: "https://www.crans.org/"
-      custom_logo_alt: "CRANS"
-    spamassassin: "SpamAssassin_crans"
-    smtphost: "smtp.adm.crans.org"
-    mynetworks: ['138.231.0.0/16', '185.230.76.0/22', '2a0c:700:0::/40']
+    certbot: '{{ loc_certbot | default(glob_certbot | default([])) }}'
+    mailman3: '{{ glob_mailman3 | default({}) | combine(loc_mailman3 | default({})) }}'
     nginx: '{{ glob_nginx | default({}) | combine(loc_nginx | default({})) }}'
+    opendkim: '{{ glob_opendkim | combine(loc_opendkim | default({})) }}'
   roles:
-    - mailman
+    - certbot
     - nginx
-
-# Deploy Mailman3
-- hosts: mailman.adm.crans.org
-  vars:
-    mailman3:
-      site_owner: root@crans.org
-      database_pass: "{{ vault.mailman3_database_pass }}"
-      restadmin_pass: "{{ vault.mailman3_restadmin_pass }}"
-      archiver_key: "{{ vault.mailman3_archiver_key }}"
-      web_secret_key: "{{ vault.mailman3_web_secret_key }}"
-      web_database_pass: "{{ vault.mailman3_web_database_pass }}"
-      web_domain: "mailman.crans.org"
-  roles:
     - mailman3
     - postfix-mailman3
+    - opendkim
diff --git a/plays/postfix.yml b/plays/postfix.yml
index 0a76001c202ec82bf8de5aeaa5266a34f2adc73e..6750239d743cea25117e1df96016cb3688c6772a 100755
--- a/plays/postfix.yml
+++ b/plays/postfix.yml
@@ -12,8 +12,7 @@
         domains: "*.crans.org"
     bind:
       masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
-    opendkim:
-        private_key: "{{ vault.opendkim_private_key }}"
+    opendkim: "{{ glob_opendkim | default({}) | combine(loc_opendkim | default({})) }}"
     policyd:
       mail: root@crans.org
       exemptions: "{{ lookup('re2oapi', 'get_role', 'user-server')[0] }}"
diff --git a/roles/mailman/handlers/main.yml b/roles/mailman/handlers/main.yml
deleted file mode 100644
index 77550456cf250ae8a15a07a5a402e41befbf9fe1..0000000000000000000000000000000000000000
--- a/roles/mailman/handlers/main.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-- name: Reload mailman
-  systemd:
-    name: mailman
-    state: reloaded
diff --git a/roles/mailman/tasks/main.yml b/roles/mailman/tasks/main.yml
deleted file mode 100644
index 9a74a41ec8e2e71289965dd8d77ec02628b7b3a7..0000000000000000000000000000000000000000
--- a/roles/mailman/tasks/main.yml
+++ /dev/null
@@ -1,47 +0,0 @@
----
-- name: Install mailman and SpamAssassin
-  apt:
-    update_cache: true
-    name:
-      - mailman
-      - spamassassin
-  register: apt_result
-  retries: 3
-  until: apt_result is succeeded
-
-- name: Deploy mailman config
-  template:
-    src: "mailman/{{ item }}.j2"
-    dest: "/etc/mailman/{{ item }}"
-    mode: 0755
-  loop:
-    - mm_cfg.py
-    - create.html
-  notify: Reload mailman
-
-- name: Deploy mailman snippet
-  template:
-    src: "nginx/snippets/fastcgi-mailman.conf.j2"
-    dest: "/etc/nginx/snippets/fastcgi-mailman.conf"
-    owner: root
-    group: root
-    mode: 0644
-
-# Fanciness
-- name: Deploy custom logo
-  copy:
-    src: "{{ mailman.custom_logo }}"
-    dest: "/usr/share/images/mailman/{{ mailman.custom_logo_name }}"
-
-- name: Deploy custom logo
-  template:
-    src: usr/lib/mailman/Mailman/htmlformat.py.j2
-    dest: /usr/lib/mailman/Mailman/htmlformat.py
-    mode: 0755
-  notify: Reload mailman
-
-- name: Indicate role in motd
-  template:
-    src: update-motd.d/05-mailman.j2
-    dest: /etc/update-motd.d/05-mailman
-    mode: 0755
diff --git a/roles/mailman/templates/mailman/create.html.j2 b/roles/mailman/templates/mailman/create.html.j2
deleted file mode 100644
index 682364028d80b704d542a92b9fb55beb0725769d..0000000000000000000000000000000000000000
--- a/roles/mailman/templates/mailman/create.html.j2
+++ /dev/null
@@ -1,13 +0,0 @@
-{{ ansible_header | comment('xml') }}
-
-<html>
-<head>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-<title>Creation de mailing list</title>
-</head>
-
-<body>
-<h1>Creation de mailing list</h1>
-Il faut s'adresser a nounou arobase crans point org.
-</body>
-</html>
diff --git a/roles/mailman/templates/mailman/mm_cfg.py.j2 b/roles/mailman/templates/mailman/mm_cfg.py.j2
deleted file mode 100644
index 25f82461115b0879dabf4c37a536487d3a908049..0000000000000000000000000000000000000000
--- a/roles/mailman/templates/mailman/mm_cfg.py.j2
+++ /dev/null
@@ -1,226 +0,0 @@
-{{ ansible_header | comment }}
-# -*- python -*-
-
-# Copyright (C) 1998,1999,2000 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301 USA
-
-
-"""This is the module which takes your site-specific settings.
-
-From a raw distribution it should be copied to mm_cfg.py.  If you
-already have an mm_cfg.py, be careful to add in only the new settings
-you want.  The complete set of distributed defaults, with annotation,
-are in ./Defaults.  In mm_cfg, override only those you want to
-change, after the
-
-  from Defaults import *
-
-line (see below).
-
-Note that these are just default settings - many can be overridden via the
-admin and user interfaces on a per-list or per-user basis.
-
-Note also that some of the settings are resolved against the active list
-setting by using the value as a format string against the
-list-instance-object's dictionary - see the distributed value of
-DEFAULT_MSG_FOOTER for an example."""
-
-
-#######################################################
-#    Here's where we get the distributed defaults.    #
-
-from Defaults import *
-
-
-#####
-# General system-wide defaults
-#####
-
-# Should image logos be used?  Set this to 0 to disable image logos from "our
-# sponsors" and just use textual links instead (this will also disable the
-# shortcut "favicon").  Otherwise, this should contain the URL base path to
-# the logo images (and must contain the trailing slash)..  If you want to
-# disable Mailman's logo footer altogther, hack
-# Mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links
-# and image names.
-IMAGE_LOGOS = '/images/mailman/'
-
-#-------------------------------------------------------------
-# The name of the list Mailman uses to send password reminders
-# and similar. Don't change if you want mailman-owner to be
-# a valid local part.
-MAILMAN_SITE_LIST = '{{ mailman.site_list }}'
-
-DEFAULT_URL= '{{ mailman.default_url }}'
-DEFAULT_URL_PATTERN = 'https://%s/'
-add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
-
-#-------------------------------------------------------------
-# Default domain for email addresses of newly created MLs
-DEFAULT_EMAIL_HOST = '{{ mailman.default_host }}'
-#-------------------------------------------------------------
-# Default host for web interface of newly created MLs
-DEFAULT_URL_HOST   = '{{ mailman.default_host }}'
-#-------------------------------------------------------------
-# Required when setting any of its arguments.
-add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
-
-#-------------------------------------------------------------
-# Do we send monthly reminders?
-DEFAULT_SEND_REMINDERS = No
-
-# Normally when a site administrator authenticates to a web page with the site
-# password, they get a cookie which authorizes them as the list admin.  It
-# makes me nervous to hand out site auth cookies because if this cookie is
-# cracked or intercepted, the intruder will have access to every list on the
-# site.  OTOH, it's dang handy to not have to re-authenticate to every list on
-# the site.  Set this value to Yes to allow site admin cookies.
-ALLOW_SITE_ADMIN_COOKIES = Yes
-
-#####
-# Archive defaults
-#####
-
-PUBLIC_ARCHIVE_URL = '{{ mailman.default_url }}archives/%(listname)s'
-
-# Are archives on or off by default?
-DEFAULT_ARCHIVE = Off
-
-# Are archives public or private by default?
-# 0=public, 1=private
-DEFAULT_ARCHIVE_PRIVATE = 1
-
-# Pipermail assumes that messages bodies contain US-ASCII text.
-# Change this option to define a different character set to be used as
-# the default character set for the archive.  The term "character set"
-# is used in MIME to refer to a method of converting a sequence of
-# octets into a sequence of characters.  If you change the default
-# charset, you might need to add it to VERBATIM_ENCODING below.
-DEFAULT_CHARSET = 'utf-8'
-
-# Most character set encodings require special HTML entity characters to be
-# quoted, otherwise they won't look right in the Pipermail archives.  However
-# some character sets must not quote these characters so that they can be
-# rendered properly in the browsers.  The primary issue is multi-byte
-# encodings where the octet 0x26 does not always represent the & character.
-# This variable contains a list of such characters sets which are not
-# HTML-quoted in the archives.
-VERBATIM_ENCODING = ['utf-8']
-
-#####
-# General defaults
-#####
-
-# The default language for this server.  Whenever we can't figure out the list
-# context or user context, we'll fall back to using this language.  See
-# LC_DESCRIPTIONS below for legal values.
-DEFAULT_SERVER_LANGUAGE = '{{ mailman.default_language }}'
-
-# How many members to display at a time on the admin cgi to unsubscribe them
-# or change their options?
-DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 50
-
-# set this variable to Yes to allow list owners to delete their own mailing
-# lists.  You may not want to give them this power, in which case, setting
-# this variable to No instead requires list removal to be done by the site
-# administrator, via the command line script bin/rmlist.
-#OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No
-
-# Set this variable to Yes to allow list owners to set the "personalized"
-# flags on their mailing lists.  Turning these on tells Mailman to send
-# separate email messages to each user instead of batching them together for
-# delivery to the MTA.  This gives each member a more personalized message,
-# but can have a heavy impact on the performance of your system.
-#OWNERS_CAN_ENABLE_PERSONALIZATION = No
-
-#####
-# List defaults.  NOTE: Changing these values does NOT change the
-# configuration of an existing list.  It only defines the default for new
-# lists you subsequently create.
-#####
-
-# Should a list, by default be advertised?  What is the default maximum number
-# of explicit recipients allowed?  What is the default maximum message size
-# allowed?
-DEFAULT_LIST_ADVERTISED = Yes
-
-# {header-name: regexp} spam filtering - we include some for example sake.
-DEFAULT_BOUNCE_MATCHING_HEADERS = """
-# Les lignes commencant par # sont des commentairtes.
-#from: .*-owner@yahoogroups.com
-#from: .*@uplinkpro.com
-#from: .*@coolstats.comic.com
-#from: .*@trafficmagnet.com
-#from: .*@hotmail.com
-#X-Reject: 450
-#X-Reject: 554
-"""
-
-# Mailman can be configured to strip any existing Reply-To: header, or simply
-# extend any existing Reply-To: with one based on the above setting.
-DEFAULT_FIRST_STRIP_REPLY_TO = Yes
-
-# SUBSCRIBE POLICY
-# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) **
-# 1 - confirmation required for subscribes
-# 2 - admin approval required for subscribes
-# 3 - both confirmation and admin approval required
-#
-# ** please do not choose option 0 if you are not allowing open
-# subscribes (next variable)
-DEFAULT_SUBSCRIBE_POLICY = 3
-
-# Is the list owner notified of subscribes/unsubscribes?
-DEFAULT_ADMIN_NOTIFY_MCHANGES = Yes
-
-# Do we send monthly reminders?
-DEFAULT_SEND_REMINDERS = No
-
-# What should happen to non-member posts which do not match explicit
-# non-member actions?
-# 0 = Accept
-# 1 = Hold
-# 2 = Reject
-# 3 = Discard
-DEFAULT_GENERIC_NONMEMBER_ACTION = 1
-
-# Use spamassassin automatically
-GLOBAL_PIPELINE.insert(5, '{{ spamassassin }}')
-# Discard messages with score higher than ...
-SPAMASSASSIN_DISCARD_SCORE = 8
-# Hold in moderation messages with score higher than ...
-SPAMASSASSIN_HOLD_SCORE = 2.1
-
-# Add SpamAssassin administration interface on gui
-# To make it work, you need to edit Gui/__init__.py
-# with
-# from SpamAssassin import SpamAssassin
-ADMIN_CATEGORIES.append("spamassassin")
-
-# Add header to keep
-PLAIN_DIGEST_KEEP_HEADERS.append('X-Spam-Score')
-
-# configure MTA
-MTA = 'Postfix'
-SMTPHOST = '{{ smtphost }}'
-SMTP_MAX_RCPTS = 50
-
-
-POSTFIX_STYLE_VIRTUAL_DOMAINS = ["{{ mailman.default_host }}"]
-
-# Note - if you're looking for something that is imported from mm_cfg, but you
-# didn't find it above, it's probably in /usr/lib/mailman/Mailman/Defaults.py.
diff --git a/roles/mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2 b/roles/mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
deleted file mode 100644
index d3215c7fa99aabdad9fe87c45bf66fb5f6bffd10..0000000000000000000000000000000000000000
--- a/roles/mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
+++ /dev/null
@@ -1,18 +0,0 @@
-{{ ansible_header | comment }}
-
-# regex to split $uri to $fastcgi_script_name and $fastcgi_path
-fastcgi_split_path_info (^/[^/]*)(.*)$;
-
-# check that the PHP script exists before passing it
-try_files $fastcgi_script_name =404;
-
-# Bypass the fact that try_files resets $fastcgi_path_info
-# see: http://trac.nginx.org/nginx/ticket/321
-set $path_info $fastcgi_path_info;
-fastcgi_param PATH_INFO $path_info;
-
-# Let NGINX handle errors
-fastcgi_intercept_errors on;
-
-include /etc/nginx/fastcgi.conf;
-fastcgi_pass unix:/var/run/fcgiwrap.socket;
diff --git a/roles/mailman/templates/update-motd.d/05-mailman.j2 b/roles/mailman/templates/update-motd.d/05-mailman.j2
deleted file mode 100755
index d3fee0db3c42e690813532b127a7346c5334f14b..0000000000000000000000000000000000000000
--- a/roles/mailman/templates/update-motd.d/05-mailman.j2
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/tail +14
-{{ ansible_header | comment }}
-> Mailman a été déployé sur cette machine. Voir /etc/mailman/ et /var/lib/mailman/.
diff --git a/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2 b/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2
deleted file mode 100644
index 3f10f131e65a394d4d6be6636297d27f640c06ee..0000000000000000000000000000000000000000
--- a/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2
+++ /dev/null
@@ -1,742 +0,0 @@
-{{ ansible_header | comment }}
-# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-
-"""Library for program-based construction of an HTML documents.
-
-Encapsulate HTML formatting directives in classes that act as containers
-for python and, recursively, for nested HTML formatting objects.
-"""
-
-
-# Eventually could abstract down to HtmlItem, which outputs an arbitrary html
-# object given start / end tags, valid options, and a value.  Ug, objects
-# shouldn't be adding their own newlines.  The next object should.
-
-
-import types
-
-from Mailman import mm_cfg
-from Mailman import Utils
-from Mailman.i18n import _, get_translation
-
-from Mailman.CSRFcheck import csrf_token
-
-SPACE = ' '
-EMPTYSTRING = ''
-NL = '\n'
-
-
-
-# Format an arbitrary object.
-def HTMLFormatObject(item, indent):
-    "Return a presentation of an object, invoking their Format method if any."
-    if type(item) == type(''):
-        return item
-    elif not hasattr(item, "Format"):
-        return `item`
-    else:
-        return item.Format(indent)
-
-def CaseInsensitiveKeyedDict(d):
-    result = {}
-    for (k,v) in d.items():
-        result[k.lower()] = v
-    return result
-
-# Given references to two dictionaries, copy the second dictionary into the
-# first one.
-def DictMerge(destination, fresh_dict):
-    for (key, value) in fresh_dict.items():
-        destination[key] = value
-
-class Table:
-    def __init__(self, **table_opts):
-        self.cells = []
-        self.cell_info = {}
-        self.row_info = {}
-        self.opts = table_opts
-
-    def AddOptions(self, opts):
-        DictMerge(self.opts, opts)
-
-    # Sets all of the cells.  It writes over whatever cells you had there
-    # previously.
-
-    def SetAllCells(self, cells):
-        self.cells = cells
-
-    # Add a new blank row at the end
-    def NewRow(self):
-        self.cells.append([])
-
-    # Add a new blank cell at the end
-    def NewCell(self):
-        self.cells[-1].append('')
-
-    def AddRow(self, row):
-        self.cells.append(row)
-
-    def AddCell(self, cell):
-        self.cells[-1].append(cell)
-
-    def AddCellInfo(self, row, col, **kws):
-        kws = CaseInsensitiveKeyedDict(kws)
-        if not self.cell_info.has_key(row):
-            self.cell_info[row] = { col : kws }
-        elif self.cell_info[row].has_key(col):
-            DictMerge(self.cell_info[row], kws)
-        else:
-            self.cell_info[row][col] = kws
-
-    def AddRowInfo(self, row, **kws):
-        kws = CaseInsensitiveKeyedDict(kws)
-        if not self.row_info.has_key(row):
-            self.row_info[row] = kws
-        else:
-            DictMerge(self.row_info[row], kws)
-
-    # What's the index for the row we just put in?
-    def GetCurrentRowIndex(self):
-        return len(self.cells)-1
-
-    # What's the index for the col we just put in?
-    def GetCurrentCellIndex(self):
-        return len(self.cells[-1])-1
-
-    def ExtractCellInfo(self, info):
-        valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan',
-                      'bgcolor']
-        output = ''
-
-        for (key, val) in info.items():
-            if not key in valid_mods:
-                continue
-            if key == 'nowrap':
-                output = output + ' NOWRAP'
-                continue
-            else:
-                output = output + ' %s="%s"' % (key.upper(), val)
-
-        return output
-
-    def ExtractRowInfo(self, info):
-        valid_mods = ['align', 'valign', 'bgcolor']
-        output = ''
-
-        for (key, val) in info.items():
-            if not key in valid_mods:
-                continue
-            output = output + ' %s="%s"' % (key.upper(), val)
-
-        return output
-
-    def ExtractTableInfo(self, info):
-        valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding',
-                      'bgcolor']
-
-        output = ''
-
-        for (key, val) in info.items():
-            if not key in valid_mods:
-                continue
-            if key == 'border' and val == None:
-                output = output + ' BORDER'
-                continue
-            else:
-                output = output + ' %s="%s"' % (key.upper(), val)
-
-        return output
-
-    def FormatCell(self, row, col, indent):
-        try:
-            my_info = self.cell_info[row][col]
-        except:
-            my_info = None
-
-        output = '\n' + ' '*indent + '<td'
-        if my_info:
-            output = output + self.ExtractCellInfo(my_info)
-        item = self.cells[row][col]
-        item_format = HTMLFormatObject(item, indent+4)
-        output = '%s>%s</td>' % (output, item_format)
-        return output
-
-    def FormatRow(self, row, indent):
-        try:
-            my_info = self.row_info[row]
-        except:
-            my_info = None
-
-        output = '\n' + ' '*indent + '<tr'
-        if my_info:
-            output = output + self.ExtractRowInfo(my_info)
-        output = output + '>'
-
-        for i in range(len(self.cells[row])):
-            output = output + self.FormatCell(row, i, indent + 2)
-
-        output = output + '\n' + ' '*indent + '</tr>'
-
-        return output
-
-    def Format(self, indent=0):
-        output = '\n' + ' '*indent + '<table'
-        output = output + self.ExtractTableInfo(self.opts)
-        output = output + '>'
-
-        for i in range(len(self.cells)):
-            output = output + self.FormatRow(i, indent + 2)
-
-        output = output + '\n' + ' '*indent + '</table>\n'
-
-        return output
-
-
-class Link:
-    def __init__(self, href, text, target=None):
-        self.href = href
-        self.text = text
-        self.target = target
-
-    def Format(self, indent=0):
-        texpr = ""
-        if self.target != None:
-            texpr = ' target="%s"' % self.target
-        return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent),
-                                          texpr,
-                                          HTMLFormatObject(self.text, indent))
-
-class FontSize:
-    """FontSize is being deprecated - use FontAttr(..., size="...") instead."""
-    def __init__(self, size, *items):
-        self.items = list(items)
-        self.size = size
-
-    def Format(self, indent=0):
-        output = '<font size="%s">' % self.size
-        for item in self.items:
-            output = output + HTMLFormatObject(item, indent)
-        output = output + '</font>'
-        return output
-
-class FontAttr:
-    """Present arbitrary font attributes."""
-    def __init__(self, *items, **kw):
-        self.items = list(items)
-        self.attrs = kw
-
-    def Format(self, indent=0):
-        seq = []
-        for k, v in self.attrs.items():
-            seq.append('%s="%s"' % (k, v))
-        output = '<font %s>' % SPACE.join(seq)
-        for item in self.items:
-            output = output + HTMLFormatObject(item, indent)
-        output = output + '</font>'
-        return output
-
-
-class Container:
-    def __init__(self, *items):
-        if not items:
-            self.items = []
-        else:
-            self.items = items
-
-    def AddItem(self, obj):
-        self.items.append(obj)
-
-    def Format(self, indent=0):
-        output = []
-        for item in self.items:
-            output.append(HTMLFormatObject(item, indent))
-        return EMPTYSTRING.join(output)
-
-
-class Label(Container):
-    align = 'right'
-
-    def __init__(self, *items):
-        Container.__init__(self, *items)
-
-    def Format(self, indent=0):
-        return ('<div align="%s">' % self.align) + \
-               Container.Format(self, indent) + \
-               '</div>'
-
-
-# My own standard document template.  YMMV.
-# something more abstract would be more work to use...
-
-class Document(Container):
-    title = None
-    language = None
-    bgcolor = mm_cfg.WEB_BG_COLOR
-    suppress_head = 0
-
-    def set_language(self, lang=None):
-        self.language = lang
-
-    def set_bgcolor(self, color):
-        self.bgcolor = color
-
-    def SetTitle(self, title):
-        self.title = title
-
-    def Format(self, indent=0, **kws):
-        charset = 'us-ascii'
-        if self.language and Utils.IsLanguage(self.language):
-            charset = Utils.GetCharSet(self.language)
-        output = ['Content-Type: text/html; charset=%s' % charset]
-        output.append('Cache-control: no-cache\n')
-        if not self.suppress_head:
-            kws.setdefault('bgcolor', self.bgcolor)
-            tab = ' ' * indent
-            output.extend([tab,
-                           '<HTML>',
-                           '<HEAD>'
-                           ])
-            if mm_cfg.IMAGE_LOGOS:
-                output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' %
-                              (mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON))
-            # Hit all the bases
-            output.append('<META http-equiv="Content-Type" '
-                          'content="text/html; charset=%s">' % charset)
-            if self.title:
-                output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
-            # Add CSS to visually hide some labeling text but allow screen
-            # readers to read it.
-            output.append("""\
-<style type="text/css">
-    div.hidden
-        {position:absolute;
-        left:-10000px;
-        top:auto;
-        width:1px;
-        height:1px;
-        overflow:hidden;}
-</style>
-""")
-            if mm_cfg.WEB_HEAD_ADD:
-                output.append(mm_cfg.WEB_HEAD_ADD)
-            output.append('%s</HEAD>' % tab)
-            quals = []
-            # Default link colors
-            if mm_cfg.WEB_VLINK_COLOR:
-                kws.setdefault('vlink', mm_cfg.WEB_VLINK_COLOR)
-            if mm_cfg.WEB_ALINK_COLOR:
-                kws.setdefault('alink', mm_cfg.WEB_ALINK_COLOR)
-            if mm_cfg.WEB_LINK_COLOR:
-                kws.setdefault('link', mm_cfg.WEB_LINK_COLOR)
-            for k, v in kws.items():
-                quals.append('%s="%s"' % (k, v))
-            output.append('%s<BODY %s' % (tab, SPACE.join(quals)))
-            # Language direction
-            direction = Utils.GetDirection(self.language)
-            output.append('dir="%s">' % direction)
-        # Always do this...
-        output.append(Container.Format(self, indent))
-        if not self.suppress_head:
-            output.append('%s</BODY>' % tab)
-            output.append('%s</HTML>' % tab)
-        return NL.join(output)
-
-    def addError(self, errmsg, tag=None):
-        if tag is None:
-            tag = _('Error: ')
-        self.AddItem(Header(3, Bold(FontAttr(
-            _(tag), color=mm_cfg.WEB_ERROR_COLOR, size='+2')).Format() +
-                            Italic(errmsg).Format()))
-
-
-class HeadlessDocument(Document):
-    """Document without head section, for templates that provide their own."""
-    suppress_head = 1
-
-
-class StdContainer(Container):
-    def Format(self, indent=0):
-        # If I don't start a new I ignore indent
-        output = '<%s>' % self.tag
-        output = output + Container.Format(self, indent)
-        output = '%s</%s>' % (output, self.tag)
-        return output
-
-
-class QuotedContainer(Container):
-    def Format(self, indent=0):
-        # If I don't start a new I ignore indent
-        output = '<%s>%s</%s>' % (
-            self.tag,
-            Utils.websafe(Container.Format(self, indent)),
-            self.tag)
-        return output
-
-class Header(StdContainer):
-    def __init__(self, num, *items):
-        self.items = items
-        self.tag = 'h%d' % num
-
-class Address(StdContainer):
-    tag = 'address'
-
-class Underline(StdContainer):
-    tag = 'u'
-
-class Bold(StdContainer):
-    tag = 'strong'
-
-class Italic(StdContainer):
-    tag = 'em'
-
-class Preformatted(QuotedContainer):
-    tag = 'pre'
-
-class Subscript(StdContainer):
-    tag = 'sub'
-
-class Superscript(StdContainer):
-    tag = 'sup'
-
-class Strikeout(StdContainer):
-    tag = 'strike'
-
-class Center(StdContainer):
-    tag = 'center'
-
-class Form(Container):
-    def __init__(self, action='', method='POST', encoding=None,
-                       mlist=None, contexts=None, user=None, *items):
-        apply(Container.__init__, (self,) +  items)
-        self.action = action
-        self.method = method
-        self.encoding = encoding
-        self.mlist = mlist
-        self.contexts = contexts
-        self.user = user
-
-    def set_action(self, action):
-        self.action = action
-
-    def Format(self, indent=0):
-        spaces = ' ' * indent
-        encoding = ''
-        if self.encoding:
-            encoding = 'enctype="%s"' % self.encoding
-        output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
-            spaces, self.action, self.method, encoding)
-        if self.mlist:
-            output = output + \
-                '<input type="hidden" name="csrf_token" value="%s">\n' \
-                % csrf_token(self.mlist, self.contexts, self.user)
-        output = output + Container.Format(self, indent+2)
-        output = '%s\n%s</FORM>\n' % (output, spaces)
-        return output
-
-
-class InputObj:
-    def __init__(self, name, ty, value, checked, **kws):
-        self.name = name
-        self.type = ty
-        self.value = value
-        self.checked = checked
-        self.kws = kws
-
-    def Format(self, indent=0):
-        charset = get_translation().charset() or 'us-ascii'
-        output = ['<INPUT name="%s" type="%s" value="%s"' %
-                  (self.name, self.type, self.value)]
-        for item in self.kws.items():
-            output.append('%s="%s"' % item)
-        if self.checked:
-            output.append('CHECKED')
-        output.append('>')
-        ret = SPACE.join(output)
-        if self.type == 'TEXT' and isinstance(ret, unicode):
-            ret = ret.encode(charset, 'xmlcharrefreplace')
-        return ret
-
-
-class SubmitButton(InputObj):
-    def __init__(self, name, button_text):
-        InputObj.__init__(self, name, "SUBMIT", button_text, checked=0)
-
-class PasswordBox(InputObj):
-    def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
-        InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size)
-
-class TextBox(InputObj):
-    def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
-        if isinstance(value, str):
-            safevalue = Utils.websafe(value)
-        else:
-            safevalue = value
-        InputObj.__init__(self, name, "TEXT", safevalue, checked=0, size=size)
-
-class Hidden(InputObj):
-    def __init__(self, name, value=''):
-        InputObj.__init__(self, name, 'HIDDEN', value, checked=0)
-
-class TextArea:
-    def __init__(self, name, text='', rows=None, cols=None, wrap='soft',
-                 readonly=0):
-        if isinstance(text, str):
-            # Double escape HTML entities in non-readonly areas.
-            doubleescape = not readonly
-            safetext = Utils.websafe(text, doubleescape)
-        else:
-            safetext = text
-        self.name = name
-        self.text = safetext
-        self.rows = rows
-        self.cols = cols
-        self.wrap = wrap
-        self.readonly = readonly
-
-    def Format(self, indent=0):
-        charset = get_translation().charset() or 'us-ascii'
-        output = '<TEXTAREA NAME=%s' % self.name
-        if self.rows:
-            output += ' ROWS=%s' % self.rows
-        if self.cols:
-            output += ' COLS=%s' % self.cols
-        if self.wrap:
-            output += ' WRAP=%s' % self.wrap
-        if self.readonly:
-            output += ' READONLY'
-        output += '>%s</TEXTAREA>' % self.text
-        if isinstance(output, unicode):
-            output = output.encode(charset, 'xmlcharrefreplace')
-        return output
-
-class FileUpload(InputObj):
-    def __init__(self, name, rows=None, cols=None, **kws):
-        apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws)
-
-class RadioButton(InputObj):
-    def __init__(self, name, value, checked=0, **kws):
-        apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws)
-
-class CheckBox(InputObj):
-    def __init__(self, name, value, checked=0, **kws):
-        apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws)
-
-class VerticalSpacer:
-    def __init__(self, size=10):
-        self.size = size
-    def Format(self, indent=0):
-        output = '<spacer type="vertical" height="%d">' % self.size
-        return output
-
-class WidgetArray:
-    Widget = None
-
-    def __init__(self, name, button_names, checked, horizontal, values):
-        self.name = name
-        self.button_names = button_names
-        self.checked = checked
-        self.horizontal = horizontal
-        self.values = values
-        assert len(values) == len(button_names)
-        # Don't assert `checked' because for RadioButtons it is a scalar while
-        # for CheckedBoxes it is a vector.  Subclasses will assert length.
-
-    def ischecked(self, i):
-        raise NotImplemented
-
-    def Format(self, indent=0):
-        t = Table(cellspacing=5)
-        items = []
-        for i, name, value in zip(range(len(self.button_names)),
-                                  self.button_names,
-                                  self.values):
-            ischecked = (self.ischecked(i))
-            item = ('<label>' +
-                    self.Widget(self.name, value, ischecked).Format() +
-                    name + '</label>')
-            items.append(item)
-            if not self.horizontal:
-                t.AddRow(items)
-                items = []
-        if self.horizontal:
-            t.AddRow(items)
-        return t.Format(indent)
-
-class RadioButtonArray(WidgetArray):
-    Widget = RadioButton
-
-    def __init__(self, name, button_names, checked=None, horizontal=1,
-                 values=None):
-        if values is None:
-            values = range(len(button_names))
-        # BAW: assert checked is a scalar...
-        WidgetArray.__init__(self, name, button_names, checked, horizontal,
-                             values)
-
-    def ischecked(self, i):
-        return self.checked == i
-
-class CheckBoxArray(WidgetArray):
-    Widget = CheckBox
-
-    def __init__(self, name, button_names, checked=None, horizontal=0,
-                 values=None):
-        if checked is None:
-            checked = [0] * len(button_names)
-        else:
-            assert len(checked) == len(button_names)
-        if values is None:
-            values = range(len(button_names))
-        WidgetArray.__init__(self, name, button_names, checked, horizontal,
-                             values)
-
-    def ischecked(self, i):
-        return self.checked[i]
-
-class UnorderedList(Container):
-    def Format(self, indent=0):
-        spaces = ' ' * indent
-        output = '\n%s<ul>\n' % spaces
-        for item in self.items:
-            output = output + '%s<li>%s\n' % \
-                     (spaces, HTMLFormatObject(item, indent + 2))
-        output = output + '%s</ul>\n' % spaces
-        return output
-
-class OrderedList(Container):
-    def Format(self, indent=0):
-        spaces = ' ' * indent
-        output = '\n%s<ol>\n' % spaces
-        for item in self.items:
-            output = output + '%s<li>%s\n' % \
-                     (spaces, HTMLFormatObject(item, indent + 2))
-        output = output + '%s</ol>\n' % spaces
-        return output
-
-class DefinitionList(Container):
-    def Format(self, indent=0):
-        spaces = ' ' * indent
-        output = '\n%s<dl>\n' % spaces
-        for dt, dd in self.items:
-            output = output + '%s<dt>%s\n<dd>%s\n' % \
-                     (spaces, HTMLFormatObject(dt, indent+2),
-                      HTMLFormatObject(dd, indent+2))
-        output = output + '%s</dl>\n' % spaces
-        return output
-
-
-
-# Logo constants
-#
-# These are the URLs which the image logos link to.  The Mailman home page now
-# points at the gnu.org site instead of the www.list.org mirror.
-#
-from mm_cfg import MAILMAN_URL
-PYTHON_URL  = 'http://www.python.org/'
-GNU_URL     = 'http://www.gnu.org/'
-CUSTOM_URL  = '{{ mailman.custom_logo_url }}'
-
-# The names of the image logo files.  These are concatentated onto
-# mm_cfg.IMAGE_LOGOS (not urljoined).
-DELIVERED_BY = 'mailman.jpg'
-PYTHON_POWERED = 'PythonPowered.png'
-GNU_HEAD = 'gnu-head-tiny.jpg'
-CUSTOM_LOGO = '{{ mailman.custom_logo_name }}'
-
-
-def MailmanLogo():
-    t = Table(border=0, width='100%')
-
-    version = mm_cfg.VERSION
-    mmlink = _("Delivered by Mailman")
-    pylink = _("Python Powered")
-    gnulink = _("GNU's Not Unix")
-    customlink = _("{{ mailman.custom_logo_alt }}")
-    if mm_cfg.SITE_LINK:
-        sitelink = mm_cfg.SITE_TEXT
-
-    if mm_cfg.IMAGE_LOGOS:
-        def logo(file, alt, base=mm_cfg.IMAGE_LOGOS):
-            return '<img src="%s" alt="%s" border="0" />' % \
-              (base + file, alt)
-        mmlink = logo(DELIVERED_BY, mmlink)
-        pylink = logo(PYTHON_POWERED, pylink)
-        gnulink = logo(GNU_HEAD, gnulink)
-        customlink = logo(CUSTOM_LOGO, customlink)
-        if mm_cfg.SITE_LINK:
-            sitelink = logo(mm_cfg.SITE_LOGO, sitelink, "")
-
-    mmlink = Link(MAILMAN_URL, mmlink + _('<br>version %(version)s'))
-    pylink = Link(PYTHON_URL, pylink)
-    gnulink = Link(GNU_URL, gnulink)
-    customlink = Link(CUSTOM_URL, customlink)
-    links = [mmlink, pylink, gnulink, customlink]
-    if mm_cfg.SITE_LINK:
-        if mm_cfg.SITE_URL:
-            sitelink = Link(mm_cfg.SITE_URL, sitelink)
-        links.append(sitelink)
-    t.AddRow(links)
-    return t
-
-
-class SelectOptions:
-   def __init__(self, varname, values, legend,
-                selected=0, size=1, multiple=None):
-      self.varname  = varname
-      self.values   = values
-      self.legend   = legend
-      self.size     = size
-      self.multiple = multiple
-      # we convert any type to tuple, commas are needed
-      if not multiple:
-         if type(selected) == types.IntType:
-             self.selected = (selected,)
-         elif type(selected) == types.TupleType:
-             self.selected = (selected[0],)
-         elif type(selected) == types.ListType:
-             self.selected = (selected[0],)
-         else:
-             self.selected = (0,)
-
-   def Format(self, indent=0):
-      spaces = " " * indent
-      items  = min( len(self.values), len(self.legend) )
-
-      # jcrey: If there is no argument, we return nothing to avoid errors
-      if items == 0:
-          return ""
-
-      text = "\n" + spaces + "<Select name=\"%s\"" % self.varname
-      if self.size > 1:
-          text = text + " size=%d" % self.size
-      if self.multiple:
-          text = text + " multiple"
-      text = text + ">\n"
-
-      for i in range(items):
-          if i in self.selected:
-              checked = " Selected"
-          else:
-              checked = ""
-
-          opt = " <option value=\"%s\"%s> %s </option>" % (
-              self.values[i], checked, self.legend[i])
-          text = text + spaces + opt + "\n"
-
-      return text + spaces + '</Select>'
diff --git a/roles/mailman3/handlers/main.yml b/roles/mailman3/handlers/main.yml
index 01c64c13fcad218496b79ba36933939618ff157a..cea846677dabedc22c205219310d8ba0a411b087 100644
--- a/roles/mailman3/handlers/main.yml
+++ b/roles/mailman3/handlers/main.yml
@@ -8,8 +8,3 @@
   service:
     name: mailman3-web
     state: restarted
-
-- name: Restart nginx
-  service:
-    name: nginx
-    state: restarted
diff --git a/roles/mailman3/tasks/main.yml b/roles/mailman3/tasks/main.yml
index 25a41d47d5fad5a729af3a8e4281dff49c1fc22f..b6f84d9aac05aacf6f80390db63057cb3c1e3df0 100644
--- a/roles/mailman3/tasks/main.yml
+++ b/roles/mailman3/tasks/main.yml
@@ -1,27 +1,28 @@
 ---
-# You will need to do after: sudo pip3 install django-allauth-cas
-# Yes, it is horrible but we need Debian Python3 to see this django app.
 - name: Install mailman3
   apt:
     update_cache: true
     name:
       - mailman3-full
-      - nginx
-      - dbconfig-no-thanks  # Do not autoconfigure database
-      - postgresql
+      - python3-ipython  # Prettier shell
       - python3-pip  # CAS
       - python3-lxml  # CAS
-      - certbot  # cert
-      - python3-certbot-nginx
+      - sassc
     install_recommends: false
   register: apt_result
   retries: 3
   until: apt_result is succeeded
 
+- name: Install Crans python modules
+  pip:
+    name: "{{ item }}"
+  loop:
+    - git+https://gitlab.crans.org/nounous/mailman-crans-theme.git
+    - git+https://gitlab.crans.org/nounous/allauth-cas-crans.git
+
 # You will need to setup postgres
 # sudo -u postgres createuser -P mailman3
 # sudo -u postgres createdb -O mailman3 mailman3
-# Test with: psql -U mailman3 -W -d mailman3 -h localhost
 - name: Configure mailman3
   template:
     src: "mailman3/{{ item }}.j2"
@@ -34,11 +35,16 @@
     - mailman-hyperkitty.cfg
   notify: Restart mailman3
 
+- name: Apply permissions to the mailman directory
+  file:
+    path: /var/lib/mailman3
+    state: directory
+    owner: list
+    group: list
+
 # You will need to setup postgres
 # sudo -u postgres createuser -P mailman3web
 # sudo -u postgres createdb -O mailman3web mailman3web
-# Test with: psql -U mailman3web -W -d mailman3web -h localhost
-# Then migrate data: sudo /usr/share/mailman3-web/manage.py migrate
 - name: Configure mailman3-web
   template:
     src: mailman3/mailman-web.py.j2
@@ -48,29 +54,29 @@
     group: www-data
   notify: Restart mailman3-web
 
-- name: Configure nginx site
-  template:
-    src: nginx/sites-available/mailman3.j2
-    dest: /etc/nginx/sites-available/mailman3
-  notify: Restart nginx
-
-- name: Enable nginx site
-  file:
-    src: /etc/nginx/sites-available/mailman3
-    dest: /etc/nginx/sites-enabled/mailman3
-    state: link
-  notify: Restart nginx
+- name: Migrate Django database
+  django_manage:
+    command: migrate
+    project_path: /usr/share/mailman3-web
+  become: true
+  become_user: www-data
+  notify: Restart mailman3-web
 
-- name: Create /etc/letsencrypt/conf.d
-  file:
-    path: /etc/letsencrypt/conf.d
-    state: directory
+- name: Collect static files
+  django_manage:
+    command: collectstatic
+    project_path: /usr/share/mailman3-web
+  become: true
+  become_user: www-data
+  notify: Restart mailman3-web
 
-- name: Add Certbot configuration
-  template:
-    src: "letsencrypt/conf.d/mailman.ini.j2"
-    dest: "/etc/letsencrypt/conf.d/mailman.ini"
-    mode: 0644
+- name: Compress static files
+  django_manage:
+    command: compress
+    project_path: /usr/share/mailman3-web
+  become: true
+  become_user: www-data
+  notify: Restart mailman3-web
 
 - name: Indicate role in motd
   template:
diff --git a/roles/mailman3/templates/letsencrypt/conf.d/mailman.ini.j2 b/roles/mailman3/templates/letsencrypt/conf.d/mailman.ini.j2
deleted file mode 100644
index a5e63741d7a67f90792f284a9094e253d6377bd3..0000000000000000000000000000000000000000
--- a/roles/mailman3/templates/letsencrypt/conf.d/mailman.ini.j2
+++ /dev/null
@@ -1,23 +0,0 @@
-{{ ansible_header | comment }}
-
-# To generate the certificate, please use the following command
-# certbot --config /etc/letsencrypt/conf.d/mailman.ini certonly
-
-# Use a 4096 bit RSA key instead of 2048
-rsa-key-size = 4096
-
-# Always use the staging/testing server
-# server = https://acme-staging.api.letsencrypt.org/directory
-
-# Uncomment and update to register with the specified e-mail address
-email = {{ mailman3.site_owner }}
-
-# Uncomment to use a text interface instead of ncurses
-text = True
-
-# Use DNS-01 challenge
-authenticator = nginx
-
-# Domains
-cert-name = mailman.crans.org
-domains = mailman.crans.org
diff --git a/roles/mailman3/templates/mailman3/mailman-web.py.j2 b/roles/mailman3/templates/mailman3/mailman-web.py.j2
index 48c4bb10f0f10446a9eebc007605ff47e4116cb4..3ee09a039d04b43b00520e294d9212734f979596 100644
--- a/roles/mailman3/templates/mailman3/mailman-web.py.j2
+++ b/roles/mailman3/templates/mailman3/mailman-web.py.j2
@@ -16,9 +16,9 @@ ADMINS = (
 # is meant to run behind a webserver reverse proxy anyway.
 ALLOWED_HOSTS = [
     "localhost",  # Archiving API from Mailman, keep it.
-    "{{ mailman3.web_domain }}",
-    # Add here all production URLs you may have.
-    #'*'
+{% for domain in mailman3.web_domains %}
+    "{{ domain }}",
+{% endfor %}
 ]
 
 # Mailman API credentials
@@ -30,12 +30,8 @@ MAILMAN_ARCHIVER_FROM = ('127.0.0.1', '::1')
 
 # Application definition
 
-# Add allauth_cas_crans path
-import sys
-sys.path.insert(0, "/usr/scripts/mailman")
-
 INSTALLED_APPS = (
-    'mailman_theme_crans',  # override templates
+    'mailman_crans_theme',  # override templates
     'hyperkitty',
     'postorius',
     'django_mailman3',
@@ -81,15 +77,15 @@ DATABASES = {
         'ENGINE': 'django.db.backends.postgresql_psycopg2',
         #'ENGINE': 'django.db.backends.mysql',
         # DB name or path to database file if using sqlite3.
-        'NAME': 'mailman3web',
+        'NAME': '{{ mailman3.web_database.name }}',
         # The following settings are not used with sqlite3:
-        'USER': 'mailman3web',
-        'PASSWORD': '{{ mailman3.web_database_pass }}',
+        'USER': '{{ mailman3.web_database.user }}',
+        'PASSWORD': '{{ mailman3.web_database.pass }}',
         # HOST: empty for localhost through domain sockets or '127.0.0.1' for
         # localhost through TCP.
-        'HOST': '127.0.0.1',
+        'HOST': '{{ mailman3.web_database.host }}',
         # PORT: set to empty string for default.
-        'PORT': '',
+        'PORT': {{ mailman3.web_database.port }},
         # OPTIONS: Extra parameters to use when connecting to the database.
         'OPTIONS': {
             # Set sql_mode to 'STRICT_TRANS_TABLES' for MySQL. See
@@ -138,21 +134,21 @@ USE_TZ = True
 
 
 # Set default domain for email addresses.
-EMAILNAME = 'crans.org'  # A changer en prod
+EMAILNAME = '{{ mailman3.default_domain }}'  # A changer en prod
 
 # If you enable internal authentication, this is the address that the emails
 # will appear to be coming from. Make sure you set a valid domain name,
 # otherwise the emails may get rejected.
 # https://docs.djangoproject.com/en/1.8/ref/settings/#default-from-email
 # DEFAULT_FROM_EMAIL = "mailing-lists@you-domain.org"
-DEFAULT_FROM_EMAIL = 'contact@{}'.format(EMAILNAME)
+DEFAULT_FROM_EMAIL = f'contact@{EMAILNAME}'
 
 # If you enable email reporting for error messages, this is where those emails
 # will appear to be coming from. Make sure you set a valid domain name,
 # otherwise the emails may get rejected.
 # https://docs.djangoproject.com/en/1.8/ref/settings/#std:setting-SERVER_EMAIL
 # SERVER_EMAIL = 'root@your-domain.org'
-SERVER_EMAIL = 'root@{}'.format(EMAILNAME)
+SERVER_EMAIL = f'root@{EMAILNAME}'
 
 
 # Django Allauth
@@ -163,7 +159,7 @@ ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
 # Social auth
 #
 SOCIALACCOUNT_PROVIDERS = {
-    'crans': {}
+    'crans': {},
     #'openid': {
     #    'SERVERS': [
     #        dict(id='yahoo',
@@ -195,15 +191,11 @@ SOCIALACCOUNT_PROVIDERS = {
 # recompiled on each requests. It means running an additional "compress"
 # management command after each code upgrade.
 # http://django-compressor.readthedocs.io/en/latest/usage/#offline-compression
+COMPRESS_PRECOMPILERS = (
+  ('text/less', 'lessc {infile} {outfile}'),
+  ('text/x-scss', 'sassc -t compressed {infile} {outfile}'),
+  ('text/x-sass', 'sassc -t compressed {infile} {outfile}'),
+)
 COMPRESS_OFFLINE = True
 
 POSTORIUS_TEMPLATE_BASE_URL = 'http://localhost/mailman3/'
-
-# Add translations, this will be useless in Debian Bullseye
-LANGUAGES = [
-    ('en', 'English'),
-    ('fr', 'Français'),
-]
-LOCALE_PATHS = [
-    '/etc/mailman3/locale',
-]
diff --git a/roles/mailman3/templates/mailman3/mailman.cfg.j2 b/roles/mailman3/templates/mailman3/mailman.cfg.j2
index 4dbccaccf154bda60de16c1bc95d42c4a19de6f2..d01a11dc7dfecefc20d237d5c700dab51c376d4f 100644
--- a/roles/mailman3/templates/mailman3/mailman.cfg.j2
+++ b/roles/mailman3/templates/mailman3/mailman.cfg.j2
@@ -172,7 +172,7 @@ class: mailman.database.postgresql.PostgreSQLDatabase
 # 'configuration' substitutions.
 #url: sqlite:///$DATA_DIR/mailman.db
 #url: mysql+pymysql://mailman3:mmpass@localhost/mailman3?charset=utf8&use_unicode=1
-url: postgres://mailman3:{{ mailman3.database_pass }}@localhost/mailman3
+url: postgres://{{ mailman3.database.user }}:{{ mailman3.database.pass }}@{{ mailman3.database.host }}:{{ mailman3.database.port }}/{{ mailman3.database.name }}
 
 debug: no
 
diff --git a/roles/mailman3/templates/nginx/sites-available/mailman3.j2 b/roles/mailman3/templates/nginx/sites-available/mailman3.j2
deleted file mode 100644
index 47ae1ebe67dbd65bf9710b21c594ef7a018e1c4a..0000000000000000000000000000000000000000
--- a/roles/mailman3/templates/nginx/sites-available/mailman3.j2
+++ /dev/null
@@ -1,76 +0,0 @@
-{{ ansible_header | comment }}
-
-upstream mailman3 {
-    server unix:/run/mailman3-web/uwsgi.sock fail_timeout=0;
-}
-
-# Local hyperkitty API
-server {
-    listen 80;
-    listen [::]:80;
-
-    server_name localhost;
-
-    location / {
-        uwsgi_pass mailman3;
-        include /etc/nginx/uwsgi_params;
-    }
-
-    # Log into separate log files
-    access_log /var/log/nginx/mailman3_access.log combined;
-    error_log /var/log/nginx/mailman3_error.log;
-}
-
-# Redirect http://mailman.crans.org to https://mailman.crans.org
-server {
-    listen 80;
-    listen [::]:80;
-
-    server_name mailman.crans.org;
-
-    location / {
-        return 302 https://$host$request_uri;
-    }
-}
-
-# Reverse proxify https://mailman.crans.org to UWSGI
-server {
-    listen 443 ssl http2;
-    listen [::]:443 ssl http2;
-
-    server_name mailman.crans.org;
-    server_tokens off;
-
-    # SSL common conf
-    ssl_certificate /etc/letsencrypt/live/mailman.crans.org/fullchain.pem;
-    ssl_certificate_key /etc/letsencrypt/live/mailman.crans.org/privkey.pem;
-    ssl_session_timeout 1d;
-    ssl_session_cache shared:MozSSL:10m;
-    ssl_session_tickets off;
-    ssl_dhparam /etc/letsencrypt/dhparam;
-    ssl_protocols TLSv1.2 TLSv1.3;
-    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
-    ssl_prefer_server_ciphers off;
-
-    # Enable OCSP Stapling, point to certificate chain
-    ssl_stapling on;
-    ssl_stapling_verify on;
-    ssl_trusted_certificate /etc/letsencrypt/live/mailman.crans.org/chain.pem;
-
-    location / {
-        uwsgi_pass mailman3;
-        include /etc/nginx/uwsgi_params;
-    }
-
-    location /mailman3/static {
-        alias /var/lib/mailman3/web/static;
-    }
-
-    location /mailman3/static/favicon.ico {
-        alias /var/lib/mailman3/web/static/postorius/img/favicon.ico;
-    }
-
-    # Log into separate log files
-    access_log /var/log/nginx/mailman3_access.log combined;
-    error_log /var/log/nginx/mailman3_error.log;
-}
diff --git a/roles/nginx/templates/nginx/sites-available/service.j2 b/roles/nginx/templates/nginx/sites-available/service.j2
index 297d069df9ccb4018012c5df5e06ae560b2294ae..66c952493fb077b9806d54cd63e69440e02c5df7 100644
--- a/roles/nginx/templates/nginx/sites-available/service.j2
+++ b/roles/nginx/templates/nginx/sites-available/service.j2
@@ -91,8 +91,8 @@ server {
     listen [::]:443{% if server.default is defined and server.default %} default_server{% endif %} ssl;
     include "/etc/nginx/snippets/options-ssl.{{ server.ssl }}.conf";
     {% else -%}
-    listen 80 default;
-    listen [::]:80 default;
+    listen 80{% if server.default is defined and server.default %} default_server{% endif %};
+    listen [::]:80{% if server.default is defined and server.default %} default_server{% endif %};
     {% endif -%}
 
     server_name {{ server.server_name|join(" ") }};
diff --git a/roles/opendkim/tasks/main.yml b/roles/opendkim/tasks/main.yml
index 6488bdb7731aa9fdbd3702936d5e1b2df22c6107..0278c4efbb338343577b3c5534848574f9d133b9 100644
--- a/roles/opendkim/tasks/main.yml
+++ b/roles/opendkim/tasks/main.yml
@@ -11,7 +11,7 @@
 
 - name: Ensure opendkim directories are here
   file:
-    path: /etc/opendkim/keys/crans.org
+    path: "/etc/opendkim/keys/{{ opendkim.domain }}"
     state: directory
     mode: 0750
     owner: opendkim
@@ -40,11 +40,11 @@
 
 - name: Deploy opendkim key
   template:
-    src: opendkim/keys/crans.org/{{ item }}.j2
-    dest: /etc/opendkim/keys/crans.org/{{ item }}
+    src: "opendkim/keys/key.{{ item }}.j2"
+    dest: "/etc/opendkim/keys/{{ opendkim.domain }}/{{ opendkim.selector }}.{{ item }}"
     mode: 0600
     owner: opendkim
     group: opendkim
   loop:
-    - mail.private
-    - mail.txt
+    - "private"
+    - "txt"
diff --git a/roles/opendkim/templates/opendkim/KeyTable.j2 b/roles/opendkim/templates/opendkim/KeyTable.j2
index 86ffcee44b86b1a0b70d3f941b9e2856df785a7d..f2d56ada73f8b8abeb9a1bfdc044a88736ffb70b 100644
--- a/roles/opendkim/templates/opendkim/KeyTable.j2
+++ b/roles/opendkim/templates/opendkim/KeyTable.j2
@@ -1 +1 @@
-mail._domainkey.crans.org crans.org:mail:/etc/opendkim/keys/crans.org/mail.private
+{{ opendkim.selector }}._domainkey.{{ opendkim.domain }} {{ opendkim.domain }}:{{ opendkim.selector }}:/etc/opendkim/keys/{{ opendkim.domain }}/{{ opendkim.selector }}.private
diff --git a/roles/opendkim/templates/opendkim/SigningTable.j2 b/roles/opendkim/templates/opendkim/SigningTable.j2
index d845dc689c0f22a408230b538a8732a18f295757..fdbc834b22c4e214bab3850d3df65dd31667d8af 100644
--- a/roles/opendkim/templates/opendkim/SigningTable.j2
+++ b/roles/opendkim/templates/opendkim/SigningTable.j2
@@ -1,2 +1,3 @@
-*@crans.org mail._domainkey.crans.org
-*@crans.eu  mail._domainkey.crans.org
+{% for pattern in opendkim.signing %}
+{{ pattern }} {{ opendkim.selector }}._domainkey.{{ opendkim.domain }}
+{% endfor %}
diff --git a/roles/opendkim/templates/opendkim/TrustedHosts.j2 b/roles/opendkim/templates/opendkim/TrustedHosts.j2
index 73c848180aafc91e6a15d0a642bfee00e298ce2e..64f8e8a9d3fb16dfc5cfe16efbf859f985ca1e8f 100644
--- a/roles/opendkim/templates/opendkim/TrustedHosts.j2
+++ b/roles/opendkim/templates/opendkim/TrustedHosts.j2
@@ -1,19 +1,3 @@
-127.0.0.1
-localhost
-::1
-
-138.231.136.0/21
-138.231.144.0/21
-
-10.231.136.0/24
-10.2.9.0/24
-
-2a0c:700:0:1::/64
-2a0c:700:0:2::/64
-2a0c:700:0:21::/64
-2a0c:700:0:22::/64
-2a0c:700:0:23::/64
-
-*.crans.org
-*.crans.fr
-*.crans.eu
+{% for host in opendkim.trust -%}
+{{ host }}
+{% endfor %}
diff --git a/roles/opendkim/templates/opendkim/keys/crans.org/mail.txt.j2 b/roles/opendkim/templates/opendkim/keys/crans.org/mail.txt.j2
deleted file mode 100644
index 9a787ee15b6fc981e356f5a37f75ac5e4b4bc3b2..0000000000000000000000000000000000000000
--- a/roles/opendkim/templates/opendkim/keys/crans.org/mail.txt.j2
+++ /dev/null
@@ -1 +0,0 @@
-mail._domainkey IN TXT "v=DKIM1; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtwkNVd9Mmz8S4WcfuPk0X2drG39gS8+uxAv8igRILgzWeN8j2hjeZesl8pm/1UTVU87bYcdfUgXiGfQy9nR5p/Vmt2kS7sXk9nsJ/VYENgb3IJQ6paWupSTFMyeKycJ4ZHCEZB/bVvifoG6vLKqW5jpsfCiOcfdcgXATn0UPuVx9t93yRrhoEMntMv9TSodjqd3FKCtJUoh5cNQHo0T6dWKtxoIgNi/mvZ92D/IACwu/XOU+Rq9fnoEI8GukBQUR5AkP0B/JrvwWXWX/3EjY8X37ljEX0XUdq/ShzTl5iK+CM83stgkFUQh/rpww5mnxYEW3X4uirJ7VJHmY4KPoIU+2DPjLQj9Hz63CMWY3Ks2pXWzxD3V+GI1aJTMFOv2LeHnI3ScqFaKj9FR4ZKMb0OW2BEFBIY3J3aeo/paRwdbVCMM7twDtZY9uInR/NhVa1v9hlOxwp4/2pGSKQYoN2CkAZ1Alzwf8M3EONLKeiC43JLYwKH1uBB1oikSVhMnLjG0219XvfG/tphyoOqJR/bCc2rdv5pLwKUl4wVuygfpvOw12bcvnTfYuk/BXzVHg9t4H8k/DJR6GAoeNAapXIS8AfAScF8QdKfplhKLJyQGJ6lQ75YD9IwRAN0oV+8NTjl46lI/C+b7mpfXCew+p6YPwfNvV2shiR0Ez8ZGUQIcCAwEAAQ==" ; ----- DKIM key mail for crans.org
diff --git a/roles/opendkim/templates/opendkim/keys/crans.org/mail.private.j2 b/roles/opendkim/templates/opendkim/keys/key.private.j2
similarity index 100%
rename from roles/opendkim/templates/opendkim/keys/crans.org/mail.private.j2
rename to roles/opendkim/templates/opendkim/keys/key.private.j2
diff --git a/roles/opendkim/templates/opendkim/keys/key.txt.j2 b/roles/opendkim/templates/opendkim/keys/key.txt.j2
new file mode 100644
index 0000000000000000000000000000000000000000..8c6fc1cf14e841e438c64b99534fc0c054e36ada
--- /dev/null
+++ b/roles/opendkim/templates/opendkim/keys/key.txt.j2
@@ -0,0 +1 @@
+{{ opendkim.txt_record }}
diff --git a/roles/postfix-mailman3/templates/postfix/main.cf.j2 b/roles/postfix-mailman3/templates/postfix/main.cf.j2
index 3cc7c11d2cb240801395a96b328bf0699071ba6f..5e1e6b363d661f9a5afcc26f21c69247ce293806 100644
--- a/roles/postfix-mailman3/templates/postfix/main.cf.j2
+++ b/roles/postfix-mailman3/templates/postfix/main.cf.j2
@@ -3,7 +3,7 @@
 # This postfix configuration set up a MTA only to send and receive mailing list mails
 
 # When a mail is sent to @localhost, this domain will be used
-myorigin = crans.org
+myorigin = {{ mailman3.postfix_domain }}
 
 smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
 biff = no
@@ -16,12 +16,16 @@ delay_warning_time = 4h
 compatibility_level = 2
 
 # TLS parameters
-smtpd_tls_cert_file=/etc/letsencrypt/live/mailman.crans.org/fullchain.pem
-smtpd_tls_key_file=/etc/letsencrypt/live/mailman.crans.org/privkey.pem
+smtpd_tls_cert_file=/etc/letsencrypt/live/{{ mailman3.postfix_domain }}/fullchain.pem
+smtpd_tls_key_file=/etc/letsencrypt/live/{{ mailman3.postfix_domain }}/privkey.pem
 smtpd_use_tls=yes
 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
 
+# OpenDKIM
+smtpd_milters = inet:localhost:12301
+non_smtpd_milters = inet:localhost:12301
+
 # See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
 # information on enabling SSL in the smtp client.
 
@@ -40,8 +44,8 @@ inet_interfaces = all
 inet_protocols = all
 
 # Do not use gethostname
-myhostname = {{ ansible_hostname }}.crans.org
-mydomain = crans.org
+myhostname = {{ ansible_hostname }}.{{ mailman3.postfix_domain }}
+mydomain = {{ mailman3.postfix_domain }}
 
 # Softbounce, ask remote mail server to send the mail again if error
 # Do not keep it active in production!
diff --git a/roles/postfix/templates/postfix/main.cf.j2 b/roles/postfix/templates/postfix/main.cf.j2
index fabff79536286da019b2b08f68e5651d2c5ba6fe..091677c8f614baacb2e9d2e72f739f037214fd6a 100644
--- a/roles/postfix/templates/postfix/main.cf.j2
+++ b/roles/postfix/templates/postfix/main.cf.j2
@@ -24,18 +24,10 @@ mydestination = {{ ansible_hostname }}, $myhostname, localhost, localhost.$mydom
 {% endif %}
 # Domaine relaye par ce MX
 relay_domains = $mydestination
-{% if postfix.mailman or postfix.public %}
                 lists.$mydomain
-{% endif %}
 {% if postfix.secondary %}
                 $mydomain, crans.ens-cachan.fr, clubs.ens-cachan.fr, install-party.ens-cachan.fr, crans.fr, crans.eu
 {% endif %}
-{% if postfix.mailman %}
-relay_recipient_maps =
-    hash:/var/local/re2o-services/mail-server/generated/virtual
-    hash:/var/lib/mailman/data/virtual-mailman
-mailman_destination_recipient_limit = 1
-{% endif %}
 # Etre notifie ou non de l'arrive de nouveaux mails
 {% if postfix.primary or postfix.secondary %}
 biff = no
@@ -48,7 +40,6 @@ biff = yes
 # il faut enlever ca.
 soft_bounce = no
 
-smtpd_reject_unlisted_sender = yes
 {% if not postfix.primary and not postfix.secondary %}
 # On delivre dans des maildir
 mail_spool_directory = /home/mail/
@@ -151,6 +142,7 @@ smtpd_sender_restrictions = permit_mynetworks
 {% endif %}
                             reject_non_fqdn_sender
                             reject_unknown_sender_domain
+                            reject_unlisted_sender
 
 ## Dit à postfix de jeter toute socket vers un serveur de policy après une
 ## utilisation. Il en recrée donc une nouvelle, ce qui permet d'éviter
diff --git a/roles/postfix/templates/postfix/master.cf.j2 b/roles/postfix/templates/postfix/master.cf.j2
index 909bbee8b71934f23b959c7f29317dcd6c215962..04ddafd74b0a41afc238596e6f7231848a244d08 100644
--- a/roles/postfix/templates/postfix/master.cf.j2
+++ b/roles/postfix/templates/postfix/master.cf.j2
@@ -140,8 +140,3 @@ scalemail-backend unix -   n   n   -   2   pipe
   flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store $${nexthop} $${user} $${extension}
 # only used by postfix-tls
 tlsmgr    unix  -       -       n       300     1       tlsmgr
-{% if postfix.mailman %}
-mailman   unix  -       n       n       -       -       pipe
-      flags=FR user=list
-      argv=/var/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user}
-{% endif %}
diff --git a/roles/postfix/templates/postfix/transport.j2 b/roles/postfix/templates/postfix/transport.j2
index 77e92b2bfd998de8d1648bf06623536b512c57b3..87cd249d67aa2ffcad63db726cddd484b6016275 100644
--- a/roles/postfix/templates/postfix/transport.j2
+++ b/roles/postfix/templates/postfix/transport.j2
@@ -1,13 +1,9 @@
 {{ ansible_header | comment }}
 # Transport des mails
 
-{% if postfix.mailman %}
-# Les mailing-listes sont delivrees localement
-lists.crans.org              mailman:
-{% else %}
-lists.crans.org              smtp:[lists.adm.crans.org]
-{% endif %}
 {% if postfix.primary or postfix.secondary %}
+# Les mailing-listes sont delivrees sur un serveur à part
+lists.crans.org              smtp:[{{ query('ldap', 'ip', 'mailman', 'adm') | ipv4 | first }}]
 # C'est le serveur des adherents qui fait les livraisons des
 # adresses clubs et adherents
 crans.org                    smtp:[users.adm.crans.org]