From b74d5e0bf1fd40c198a65adb664f0d00a70bf39e Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <ynerant@crans.org>
Date: Sun, 28 Feb 2021 17:46:16 +0100
Subject: [PATCH] [mailman3] Drop mailman2 configuration

Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
---
 group_vars/mailman.yml                        |  61 --
 hosts                                         |   3 -
 plays/mailman.yml                             |  20 -
 roles/mailman/handlers/main.yml               |   5 -
 roles/mailman/tasks/main.yml                  |  47 --
 .../mailman/templates/mailman/create.html.j2  |  13 -
 roles/mailman/templates/mailman/mm_cfg.py.j2  | 226 ------
 .../nginx/snippets/fastcgi-mailman.conf.j2    |  18 -
 .../templates/update-motd.d/05-mailman.j2     |   3 -
 .../usr/lib/mailman/Mailman/htmlformat.py.j2  | 742 ------------------
 10 files changed, 1138 deletions(-)
 delete mode 100644 roles/mailman/handlers/main.yml
 delete mode 100644 roles/mailman/tasks/main.yml
 delete mode 100644 roles/mailman/templates/mailman/create.html.j2
 delete mode 100644 roles/mailman/templates/mailman/mm_cfg.py.j2
 delete mode 100644 roles/mailman/templates/nginx/snippets/fastcgi-mailman.conf.j2
 delete mode 100755 roles/mailman/templates/update-motd.d/05-mailman.j2
 delete mode 100644 roles/mailman/templates/usr/lib/mailman/Mailman/htmlformat.py.j2

diff --git a/group_vars/mailman.yml b/group_vars/mailman.yml
index c9813ae2..d101e33b 100644
--- a/group_vars/mailman.yml
+++ b/group_vars/mailman.yml
@@ -7,67 +7,6 @@ loc_certbot:
     certname: crans.org
     domains: "*.crans.org"
 
-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
-  servers:
-    - server_name:
-      - lists.crans.org
-      ssl: crans.org
-      root: "/usr/lib/cgi-bin/mailman/"
-      index:
-        - index.htm
-        - index.html
-      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"
-          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"
-          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"
-          params:
-            - "alias /usr/share/images/mailman"
-        - filter: "/robots.txt"
-          params:
-            - "alias /var/www/robots.txt"
-        - filter: "/archives"
-          params:
-            - "alias /var/lib/mailman/archives/public"
-            - "autoindex on"
-
 glob_mailman3:
   site_owner: root@crans.org
   database:
diff --git a/hosts b/hosts
index 782d8eee..58e24a4a 100644
--- a/hosts
+++ b/hosts
@@ -89,9 +89,6 @@ sputnik.adm.crans.org
 [linx]
 linx.adm.crans.org
 
-[mailman]
-redisdead.adm.crans.org
-
 [mailman]
 mailman.adm.crans.org
 
diff --git a/plays/mailman.yml b/plays/mailman.yml
index a115d96c..2182e778 100755
--- a/plays/mailman.yml
+++ b/plays/mailman.yml
@@ -1,25 +1,5 @@
 #!/usr/bin/env ansible-playbook
 ---
-# Deploy Mailman
-- hosts: redisdead.adm.crans.org
-  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']
-    nginx: '{{ glob_nginx | default({}) | combine(loc_nginx | default({})) }}'
-  roles:
-    - mailman
-    - nginx
-
 # Deploy Mailman3
 - hosts: mailman
   vars:
diff --git a/roles/mailman/handlers/main.yml b/roles/mailman/handlers/main.yml
deleted file mode 100644
index 77550456..00000000
--- 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 9a74a41e..00000000
--- 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 68236402..00000000
--- 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 25f82461..00000000
--- 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 d3215c7f..00000000
--- 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 d3fee0db..00000000
--- 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 3f10f131..00000000
--- 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>'
-- 
GitLab