Huge clean up in mailman configuration
parent
e585efb9af
commit
4ebcfa287a
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Reload mailman
|
||||||
|
systemd:
|
||||||
|
name: mailman
|
||||||
|
state: reloaded
|
|
@ -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
|
|
@ -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>
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/tail +14
|
||||||
|
{{ ansible_header | comment }}
|
||||||
|
[0m> [38;5;82mMailman[0m a été déployé sur cette machine. Voir [38;5;6m/etc/mailman/[0m et [38;5;6m/var/lib/mailman/[0m.
|
|
@ -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>'
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- name: Reload nginx
|
||||||
|
systemd:
|
||||||
|
name: nginx
|
||||||
|
state: reloaded
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{ ansible_header | comment }}
|
||||||
|
Stop:$apr1$NXaV5H7Q$J3ora3Jo5h775Y1nm93PN1
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 }};
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/tail +14
|
||||||
|
{{ ansible_header | comment }}
|
||||||
|
[0m> [38;5;82mNGINX[0m a été déployé sur cette machine. Voir [38;5;6m/etc/nginx/[0m.
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{ ansible_header | comment }}
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
Loading…
Reference in New Issue