From ca9b60e34617f0f9ddbb5cdaa59c843aedc4b7f5 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 17 May 2020 08:30:26 +0200
Subject: [PATCH 1/6] Clean at and monit

---
 clean_servers.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/clean_servers.yml b/clean_servers.yml
index 218948f2..4f727380 100755
--- a/clean_servers.yml
+++ b/clean_servers.yml
@@ -9,6 +9,7 @@
       apt:
         state: absent
         name:
+          - at
           - arpwatch  # old sniffing
           - collectd
           - collectd-utils  # old monitoring
@@ -28,6 +29,7 @@
           - monitoring-plugins-standard
           - monitoring-plugins-basic
           - monitoring-plugins-common
+          - monit
           - libmonitoring-plugin-perl
           - snmp
           - nagios-plugins-contrib

From 41e941034e0a7efc5cbd6134094eb6aaf11be378 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 17 May 2020 08:32:29 +0200
Subject: [PATCH 2/6] [reverseproxy] Do not install nginx certbot

---
 roles/nginx-reverseproxy/tasks/main.yml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/roles/nginx-reverseproxy/tasks/main.yml b/roles/nginx-reverseproxy/tasks/main.yml
index 5a0e298f..b1e39458 100644
--- a/roles/nginx-reverseproxy/tasks/main.yml
+++ b/roles/nginx-reverseproxy/tasks/main.yml
@@ -2,9 +2,7 @@
 - name: Install NGINX
   apt:
     update_cache: true
-    name:
-      - nginx
-      - python3-certbot-nginx  # for options-ssl-nginx.conf
+    name: nginx
   register: apt_result
   retries: 3
   until: apt_result is succeeded

From 00c5769d6e930294b6a76551db46e1d55b195bf2 Mon Sep 17 00:00:00 2001
From: Bombar Maxime <bombar@crans.org>
Date: Sun, 17 May 2020 09:05:32 +0200
Subject: [PATCH 3/6] [clean_servers] Clean up nagios.

---
 clean_servers.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/clean_servers.yml b/clean_servers.yml
index 4f727380..0b6b7fd0 100755
--- a/clean_servers.yml
+++ b/clean_servers.yml
@@ -85,9 +85,12 @@
         - /etc/munin
         - /etc/icinga2
         - /etc/init.d/bcfg2
+        - /etc/nagios
+        - /etc/nagios-plugins
         - /etc/nut
         - /etc/nginx/sites-enabled/status
         - /etc/nginx/sites-available/status
+        - /etc/pnp4nagios
         - /var/local/aptdater
         - /etc/apt-dater-host.conf
         - /etc/sudoers.d/apt-dater-host

From 79f4d274b07f80f585dfcac8bb41fc77980a9aa7 Mon Sep 17 00:00:00 2001
From: Bombar Maxime <bombar@crans.org>
Date: Sun, 17 May 2020 09:05:53 +0200
Subject: [PATCH 4/6] Wildcard certificate on redisdead

---
 certbot.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/certbot.yml b/certbot.yml
index 6a6a3eb5..80f49ebc 100755
--- a/certbot.yml
+++ b/certbot.yml
@@ -2,7 +2,7 @@
 ---
 # Temporary
 # Wildcard certificate for MX servers
-- hosts: titanic.adm.crans.org
+- hosts: titanic.adm.crans.org, redisdead.adm.crans.org
   vars:
     certbot:
       dns_rfc2136_name: certbot_challenge.

From e585efb9afdba643ebc577d3d2acce575ffe7c10 Mon Sep 17 00:00:00 2001
From: Bombar Maxime <bombar@crans.org>
Date: Sun, 17 May 2020 09:06:20 +0200
Subject: [PATCH 5/6] Add apt-file to common tools

---
 roles/common-tools/tasks/main.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/roles/common-tools/tasks/main.yml b/roles/common-tools/tasks/main.yml
index 70488e80..b92fea69 100644
--- a/roles/common-tools/tasks/main.yml
+++ b/roles/common-tools/tasks/main.yml
@@ -4,6 +4,7 @@
     update_cache: true
     install_recommends: false
     name:
+      - apt-file
       - sudo
       - molly-guard  # prevent reboot
       - ntp  # network time sync

From 4ebcfa287a2cbc8c0dfbf50cfc8261f8bc73839e Mon Sep 17 00:00:00 2001
From: Bombar Maxime <bombar@crans.org>
Date: Sun, 17 May 2020 11:09:23 +0200
Subject: [PATCH 6/6] Huge clean up in mailman configuration

---
 logos/crans.png                               | Bin 0 -> 10618 bytes
 mailman.yml                                   |  23 +
 roles/mailman/handlers/main.yml               |   5 +
 roles/mailman/tasks/main.yml                  |  39 +
 .../mailman/templates/mailman/create.html.j2  |  13 +
 roles/mailman/templates/mailman/mm_cfg.py.j2  | 226 ++++++
 .../templates/update-motd.d/05-mailman.j2     |   3 +
 .../usr/lib/mailman/Mailman/htmlformat.py.j2  | 742 ++++++++++++++++++
 roles/nginx-mailman/handlers/main.yml         |   5 +
 roles/nginx-mailman/tasks/main.yml            |  43 +
 .../templates/nginx/mailman_passwd.j2         |   2 +
 .../nginx/sites-available/mailman.j2          |  94 +++
 .../nginx/snippets/fastcgi-mailman.conf.j2    |  18 +
 .../nginx/snippets/fastcgi-mailman.conf.j2~   |  18 +
 .../nginx/snippets/options-ssl.conf.j2        |  17 +
 .../templates/update-motd.d/05-service.j2     |   3 +
 .../templates/var/www/custom_401.html.j2      |  18 +
 .../templates/var/www/robots.txt.j2           |   4 +
 18 files changed, 1273 insertions(+)
 create mode 100644 logos/crans.png
 create mode 100755 mailman.yml
 create mode 100644 roles/mailman/handlers/main.yml
 create mode 100644 roles/mailman/tasks/main.yml
 create mode 100644 roles/mailman/templates/mailman/create.html.j2
 create mode 100644 roles/mailman/templates/mailman/mm_cfg.py.j2
 create mode 100755 roles/mailman/templates/update-motd.d/05-mailman.j2
 create mode 100644 roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2
 create mode 100644 roles/nginx-mailman/handlers/main.yml
 create mode 100644 roles/nginx-mailman/tasks/main.yml
 create mode 100644 roles/nginx-mailman/templates/nginx/mailman_passwd.j2
 create mode 100644 roles/nginx-mailman/templates/nginx/sites-available/mailman.j2
 create mode 100644 roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
 create mode 100644 roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2~
 create mode 100644 roles/nginx-mailman/templates/nginx/snippets/options-ssl.conf.j2
 create mode 100755 roles/nginx-mailman/templates/update-motd.d/05-service.j2
 create mode 100644 roles/nginx-mailman/templates/var/www/custom_401.html.j2
 create mode 100644 roles/nginx-mailman/templates/var/www/robots.txt.j2

diff --git a/logos/crans.png b/logos/crans.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c5e281a69694f2aed73c8466229c3dcb96b9609
GIT binary patch
literal 10618
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
UD3FYmR{#J207*qoM6N<$g4PgwlmGw#

literal 0
HcmV?d00001

diff --git a/mailman.yml b/mailman.yml
new file mode 100755
index 00000000..2ce0e772
--- /dev/null
+++ b/mailman.yml
@@ -0,0 +1,23 @@
+#!/usr/bin/env ansible-playbook
+# Mailman playbook
+---
+- hosts: redisdead.adm.crans.org
+  vars:
+    mailman:
+      site_list: "nounou"
+      default_url: "https://lists.crans.org/"
+      default_host: "lists.crans.org"
+      default_language: "fr"
+      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.";
+    spamassassin: "SpamAssassin_crans"
+    smtphost: "smtp.adm.crans.org"
+    mynetworks: ['138.231.0.0/16', '185.230.76.0/22', '2a0c:700:0::/40']
+    nginx:
+      ssl:
+        cert: /etc/letsencrypt/live/crans.org/fullchain.pem
+        key: /etc/letsencrypt/live/crans.org/privkey.pem
+        trusted_cert: /etc/letsencrypt/live/crans.org/chain.pem
+  roles:
+    - mailman
+    - nginx-mailman
diff --git a/roles/mailman/handlers/main.yml b/roles/mailman/handlers/main.yml
new file mode 100644
index 00000000..77550456
--- /dev/null
+++ b/roles/mailman/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+- name: Reload mailman
+  systemd:
+    name: mailman
+    state: reloaded
diff --git a/roles/mailman/tasks/main.yml b/roles/mailman/tasks/main.yml
new file mode 100644
index 00000000..53ae09de
--- /dev/null
+++ b/roles/mailman/tasks/main.yml
@@ -0,0 +1,39 @@
+---
+- 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
+
+# Fanciness
+- name: Deploy crans logo
+  copy:
+    src: ../../../logos/crans.png
+    dest: /usr/share/images/mailman/crans.png
+
+- name: Deploy crans 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
new file mode 100644
index 00000000..68236402
--- /dev/null
+++ b/roles/mailman/templates/mailman/create.html.j2
@@ -0,0 +1,13 @@
+{{ 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
new file mode 100644
index 00000000..25f82461
--- /dev/null
+++ b/roles/mailman/templates/mailman/mm_cfg.py.j2
@@ -0,0 +1,226 @@
+{{ 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/update-motd.d/05-mailman.j2 b/roles/mailman/templates/update-motd.d/05-mailman.j2
new file mode 100755
index 00000000..d3fee0db
--- /dev/null
+++ b/roles/mailman/templates/update-motd.d/05-mailman.j2
@@ -0,0 +1,3 @@
+#!/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
new file mode 100644
index 00000000..146f9576
--- /dev/null
+++ b/roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2
@@ -0,0 +1,742 @@
+{{ 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/'
+CRANS_URL   = 'http://www.crans.org/'
+
+# 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'
+CRANS_LOGO = 'crans.png'
+
+
+def MailmanLogo():
+    t = Table(border=0, width='100%')
+
+    version = mm_cfg.VERSION
+    mmlink = _("Delivered by Mailman")
+    pylink = _("Python Powered")
+    gnulink = _("GNU's Not Unix")
+    cranslink = _("CRANS")
+    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)
+        cranslink = logo(CRANS_LOGO, cranslink)
+        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)
+    cranslink = Link(CRANS_URL, cranslink)
+    links = [mmlink, pylink, gnulink, cranslink]
+    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/nginx-mailman/handlers/main.yml b/roles/nginx-mailman/handlers/main.yml
new file mode 100644
index 00000000..6dfcdd76
--- /dev/null
+++ b/roles/nginx-mailman/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+- name: Reload nginx
+  systemd:
+    name: nginx
+    state: reloaded
diff --git a/roles/nginx-mailman/tasks/main.yml b/roles/nginx-mailman/tasks/main.yml
new file mode 100644
index 00000000..e2036b6b
--- /dev/null
+++ b/roles/nginx-mailman/tasks/main.yml
@@ -0,0 +1,43 @@
+---
+- name: Install NGINX
+  apt:
+    update_cache: true
+    name:
+      - nginx
+  register: apt_result
+  retries: 3
+  until: apt_result is succeeded
+
+- name: Copy configuration files
+  template:
+    src: "{{ item.src }}"
+    dest: "{{ item.dest }}"
+  loop:
+    - src: nginx/sites-available/mailman.j2
+      dest: /etc/nginx/sites-available/mailman
+    - src: nginx/mailman_passwd.j2
+      dest: /etc/nginx/mailman_passwd
+    - src: nginx/snippets/fastcgi-mailman.conf.j2
+      dest: /etc/nginx/snippets/fastcgi-mailman.conf
+    - src: nginx/snippets/options-ssl.conf.j2
+      dest: /etc/nginx/snippets/options-ssl.conf
+    - src: var/www/robots.txt.j2
+      dest: /var/www/robots.txt
+    - src: var/www/custom_401.html.j2
+      dest: /var/www/custom_401.html
+  notify: Reload nginx
+
+- name: Enable mailman
+  file:
+    src: /etc/nginx/sites-available/mailman
+    dest: /etc/nginx/sites-enabled/mailman
+    state: link
+    force: true
+  when: not ansible_check_mode
+  notify: Reload nginx
+
+- name: Indicate role in motd
+  template:
+    src: update-motd.d/05-service.j2
+    dest: /etc/update-motd.d/05-nginx-mailman
+    mode: 0755
diff --git a/roles/nginx-mailman/templates/nginx/mailman_passwd.j2 b/roles/nginx-mailman/templates/nginx/mailman_passwd.j2
new file mode 100644
index 00000000..741d52d9
--- /dev/null
+++ b/roles/nginx-mailman/templates/nginx/mailman_passwd.j2
@@ -0,0 +1,2 @@
+{{ ansible_header | comment }}
+Stop:$apr1$NXaV5H7Q$J3ora3Jo5h775Y1nm93PN1
diff --git a/roles/nginx-mailman/templates/nginx/sites-available/mailman.j2 b/roles/nginx-mailman/templates/nginx/sites-available/mailman.j2
new file mode 100644
index 00000000..ba13c111
--- /dev/null
+++ b/roles/nginx-mailman/templates/nginx/sites-available/mailman.j2
@@ -0,0 +1,94 @@
+{{ ansible_header | comment }}
+server {
+	listen 80 default;
+	listen [::]:80 default;
+
+	server_name _;
+
+	location / {
+	    return 302 https://{{ mailman.default_host }}$request_uri;
+	}
+}
+
+# Redirect everybody to mailing lists
+server {
+	listen 443 default_server ssl;
+	listen [::]:443 default_server ssl;
+	server_name _;
+
+	include "/etc/nginx/snippets/options-ssl.conf";
+
+	location / {
+		 return 302 https://{{ mailman.default_host }}$request_uri;
+	}
+}
+
+server {
+	listen 443 ssl http2;
+	listen [::]:443 ssl http2;
+	server_name {{ mailman.default_host }};
+
+	include "/etc/nginx/snippets/options-ssl.conf";
+
+	root /usr/lib/cgi-bin/mailman/;
+	index index.htm index.html;
+
+        location /error/ {
+		internal;
+		alias /var/www/;
+        }
+
+	location /create {
+		default_type text/html;
+		alias /etc/mailman/create.html;
+	}
+
+	location ~ ^/$ {
+		return 302 https://{{ mailman.default_host }}/listinfo;
+	}
+
+	location / {
+		include "/etc/nginx/snippets/fastcgi-mailman.conf";
+	}
+
+        location ~ ^/listinfo {
+                satisfy any;
+		include "/etc/nginx/snippets/fastcgi-mailman.conf";
+
+		{% for net in mynetworks -%}
+                allow {{ net }};
+		{% endfor -%}
+                deny all;
+
+	        auth_basic {{ mailman.auth_basic }}
+		auth_basic_user_file /etc/nginx/mailman_passwd;
+
+		error_page 401 /error/custom_401.html;
+        }
+
+        location ~ ^/admin {
+                satisfy any;
+
+		include "/etc/nginx/snippets/fastcgi-mailman.conf";
+
+		{% for net in mynetworks -%}
+                allow {{ net }};
+		{% endfor -%}
+                deny all;
+
+	        auth_basic {{ mailman.auth_basic }}
+		auth_basic_user_file /etc/nginx/mailman_passwd;
+		error_page 401 /error/custom_401.html;
+        }
+
+
+	location /images/mailman { alias /usr/share/images/mailman;}
+
+	location /robots.txt { alias /var/www/robots.txt;}
+
+	location /archives {
+		alias /var/lib/mailman/archives/public;
+		autoindex on;
+	}
+
+}
diff --git a/roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2 b/roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
new file mode 100644
index 00000000..d3215c7f
--- /dev/null
+++ b/roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
@@ -0,0 +1,18 @@
+{{ 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/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2~ b/roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2~
new file mode 100644
index 00000000..3ce2f923
--- /dev/null
+++ b/roles/nginx-mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2~
@@ -0,0 +1,18 @@
+{{ 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;
\ No newline at end of file
diff --git a/roles/nginx-mailman/templates/nginx/snippets/options-ssl.conf.j2 b/roles/nginx-mailman/templates/nginx/snippets/options-ssl.conf.j2
new file mode 100644
index 00000000..79d75395
--- /dev/null
+++ b/roles/nginx-mailman/templates/nginx/snippets/options-ssl.conf.j2
@@ -0,0 +1,17 @@
+{{ ansible_header | comment }}
+
+ssl_certificate {{ nginx.ssl.cert }};
+ssl_certificate_key {{ nginx.ssl.key }};
+ssl_session_timeout 1d;
+ssl_session_cache shared:MozSSL:10m;
+ssl_session_tickets off;
+ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+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 {{ nginx.ssl.trusted_cert }};
diff --git a/roles/nginx-mailman/templates/update-motd.d/05-service.j2 b/roles/nginx-mailman/templates/update-motd.d/05-service.j2
new file mode 100755
index 00000000..82373d0b
--- /dev/null
+++ b/roles/nginx-mailman/templates/update-motd.d/05-service.j2
@@ -0,0 +1,3 @@
+#!/usr/bin/tail +14
+{{ ansible_header | comment }}
+> NGINX a été déployé sur cette machine. Voir /etc/nginx/.
diff --git a/roles/nginx-mailman/templates/var/www/custom_401.html.j2 b/roles/nginx-mailman/templates/var/www/custom_401.html.j2
new file mode 100644
index 00000000..93fc38ac
--- /dev/null
+++ b/roles/nginx-mailman/templates/var/www/custom_401.html.j2
@@ -0,0 +1,18 @@
+{{ ansible_header | comment('xml') }}
+
+<html>
+<head>
+  <title>Accès refusé</title>
+  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+</head>
+<body>
+  <h1>Accès refusé</h1>
+  <p>
+    Pour éviter le scan des adresses de diffusions par un robot, cette page demande un identifiant et mot de passe.
+  </p>
+  <ul>
+    <li>Identifiant : <em>Stop</em></li>
+    <li>Mot de passe : <em>Spam</em></li>
+  </ul>
+</body>
+</html>
diff --git a/roles/nginx-mailman/templates/var/www/robots.txt.j2 b/roles/nginx-mailman/templates/var/www/robots.txt.j2
new file mode 100644
index 00000000..3fbaed74
--- /dev/null
+++ b/roles/nginx-mailman/templates/var/www/robots.txt.j2
@@ -0,0 +1,4 @@
+{{ ansible_header | comment }}
+
+User-agent: *
+Disallow: /