summaryrefslogtreecommitdiff
path: root/src/mailman/rest/docs
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/rest/docs')
-rw-r--r--src/mailman/rest/docs/__init__.py97
-rw-r--r--src/mailman/rest/docs/domains.rst38
-rw-r--r--src/mailman/rest/docs/listconf.rst13
-rw-r--r--src/mailman/rest/docs/systemconf.rst1
-rw-r--r--src/mailman/rest/docs/templates.rst539
5 files changed, 651 insertions, 37 deletions
diff --git a/src/mailman/rest/docs/__init__.py b/src/mailman/rest/docs/__init__.py
index 113e299c3..5a039ac3d 100644
--- a/src/mailman/rest/docs/__init__.py
+++ b/src/mailman/rest/docs/__init__.py
@@ -17,8 +17,103 @@
"""Doctest layer setup."""
+import threading
+
+from http.server import BaseHTTPRequestHandler, HTTPServer
from mailman import public
+from mailman.testing.helpers import wait_for_webservice
from mailman.testing.layers import RESTLayer
-public(layer=RESTLayer)
+# New in Python 3.5.
+try:
+ from http import HTTPStatus
+except ImportError: # pragma: no cover
+ class HTTPStatus:
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+ OK = 200
+
+
+# We need a web server to vend non-mailman: urls.
+class TestableHandler(BaseHTTPRequestHandler):
+ # Be quiet.
+ def log_request(*args, **kws):
+ pass
+
+ log_error = log_request
+
+ def do_GET(self): # pragma: no cover
+ if self.path == '/welcome_2.txt':
+ if self.headers['Authorization'] != 'Basic YW5uZTppcyBzcGVjaWFs':
+ self.send_error(HTTPStatus.FORBIDDEN)
+ return
+ response = TEXTS.get(self.path)
+ if response is None:
+ self.send_error(HTTPStatus.NOT_FOUND) # pragma: no cover
+ return
+ self.send_response(HTTPStatus.OK)
+ self.send_header('Content-Type', 'UTF-8')
+ self.end_headers()
+ self.wfile.write(response.encode('utf-8'))
+
+
+class HTTPLayer(RESTLayer):
+ httpd = None
+
+ @classmethod
+ def setUp(cls):
+ assert cls.httpd is None, 'Layer already set up'
+ cls.httpd = HTTPServer(('localhost', 8180), TestableHandler)
+ cls._thread = threading.Thread(target=cls.httpd.serve_forever)
+ cls._thread.daemon = True
+ cls._thread.start()
+ wait_for_webservice('localhost', 8180)
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.httpd is not None, 'Layer not set up'
+ cls.httpd.shutdown()
+ cls.httpd.server_close()
+ cls._thread.join()
+
+
+public(layer=HTTPLayer)
+
+
+# Response texts.
+WELCOME_1 = """\
+Welcome to the "$list_name" mailing list!
+
+To post to this list, send your email to:
+
+ $fqdn_listname
+
+There is a Code of Conduct for this mailing list which you can view at
+http://www.example.com/code-of-conduct.html
+"""
+
+WELCOME_2 = """\
+I'm glad you made it!
+"""
+
+WELCOME_3 = """\
+Je suis heureux que vous pouvez nous rejoindre!
+"""
+
+WELCOME_4 = """\
+Welcome to the $list_name list in the $domain domain.
+"""
+
+WELCOME_5 = """\
+Yay! You joined the $fqdn_listname mailing list.
+"""
+
+
+TEXTS = {
+ '/welcome_1.txt': WELCOME_1,
+ '/welcome_2.txt': WELCOME_2,
+ '/ant.example.com/fr/welcome_3.txt': WELCOME_3,
+ '/welcome_4.txt': WELCOME_4,
+ '/welcome_5.txt': WELCOME_5,
+ }
diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst
index 89c03d170..82b7dc7cd 100644
--- a/src/mailman/rest/docs/domains.rst
+++ b/src/mailman/rest/docs/domains.rst
@@ -26,19 +26,16 @@ initially none.
Once a domain is added, it is accessible through the API.
::
- >>> domain_manager.add(
- ... 'example.com', 'An example domain', 'http://lists.example.com')
- <Domain example.com, An example domain, base_url: http://lists.example.com>
+ >>> domain_manager.add('example.com', 'An example domain')
+ <Domain example.com, An example domain>
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/domains')
entry 0:
- base_url: http://lists.example.com
description: An example domain
http_etag: "..."
mail_host: example.com
self_link: http://localhost:9001/3.0/domains/example.com
- url_host: lists.example.com
http_etag: "..."
start: 0
total_size: 1
@@ -46,39 +43,30 @@ Once a domain is added, it is accessible through the API.
At the top level, all domains are returned as separate entries.
::
- >>> domain_manager.add(
- ... 'example.org',
- ... base_url='http://mail.example.org')
- <Domain example.org, base_url: http://mail.example.org>
+ >>> domain_manager.add('example.org',)
+ <Domain example.org>
>>> domain_manager.add(
... 'lists.example.net',
- ... 'Porkmasters',
- ... 'http://example.net')
- <Domain lists.example.net, Porkmasters, base_url: http://example.net>
+ ... 'Porkmasters')
+ <Domain lists.example.net, Porkmasters>
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/domains')
entry 0:
- base_url: http://lists.example.com
description: An example domain
http_etag: "..."
mail_host: example.com
self_link: http://localhost:9001/3.0/domains/example.com
- url_host: lists.example.com
entry 1:
- base_url: http://mail.example.org
description: None
http_etag: "..."
mail_host: example.org
self_link: http://localhost:9001/3.0/domains/example.org
- url_host: mail.example.org
entry 2:
- base_url: http://example.net
description: Porkmasters
http_etag: "..."
mail_host: lists.example.net
self_link: http://localhost:9001/3.0/domains/lists.example.net
- url_host: example.net
http_etag: "..."
start: 0
total_size: 3
@@ -91,12 +79,10 @@ The information for a single domain is available by following one of the
``self_links`` from the above collection.
>>> dump_json('http://localhost:9001/3.0/domains/lists.example.net')
- base_url: http://example.net
description: Porkmasters
http_etag: "..."
mail_host: lists.example.net
self_link: http://localhost:9001/3.0/domains/lists.example.net
- url_host: example.net
You can also list all the mailing lists for a given domain. At first, the
example.com domain does not contain any mailing lists.
@@ -154,30 +140,26 @@ New domains can be created by posting to the ``domains`` url.
Now the web service knows about our new domain.
>>> dump_json('http://localhost:9001/3.0/domains/lists.example.com')
- base_url: http://lists.example.com
description: None
http_etag: "..."
mail_host: lists.example.com
self_link: http://localhost:9001/3.0/domains/lists.example.com
- url_host: lists.example.com
And the new domain is in our database.
::
>>> domain_manager['lists.example.com']
- <Domain lists.example.com, base_url: http://lists.example.com>
+ <Domain lists.example.com>
# Unlock the database.
>>> transaction.abort()
-You can also create a new domain with a description, a base url, and a contact
-address.
+You can also create a new domain with a description and a contact address.
::
>>> dump_json('http://localhost:9001/3.0/domains', {
... 'mail_host': 'my.example.com',
... 'description': 'My new domain',
- ... 'base_url': 'http://allmy.example.com'
... })
content-length: 0
content-type: application/json; charset=UTF-8
@@ -186,15 +168,13 @@ address.
...
>>> dump_json('http://localhost:9001/3.0/domains/my.example.com')
- base_url: http://allmy.example.com
description: My new domain
http_etag: "..."
mail_host: my.example.com
self_link: http://localhost:9001/3.0/domains/my.example.com
- url_host: allmy.example.com
>>> domain_manager['my.example.com']
- <Domain my.example.com, My new domain, base_url: http://allmy.example.com>
+ <Domain my.example.com, My new domain>
# Unlock the database.
>>> transaction.abort()
diff --git a/src/mailman/rest/docs/listconf.rst b/src/mailman/rest/docs/listconf.rst
index 6804644ac..922da5ea4 100644
--- a/src/mailman/rest/docs/listconf.rst
+++ b/src/mailman/rest/docs/listconf.rst
@@ -36,6 +36,8 @@ All readable attributes for a list are available on a sub-resource.
default_member_action: defer
default_nonmember_action: hold
description:
+ digest_footer_uri:
+ digest_header_uri:
digest_last_sent_at: None
digest_send_periodic: True
digest_size_threshold: 30.0
@@ -44,8 +46,10 @@ All readable attributes for a list are available on a sub-resource.
display_name: Ant
filter_content: False
first_strip_reply_to: False
+ footer_uri:
fqdn_listname: ant@example.com
goodbye_message_uri:
+ header_uri:
http_etag: "..."
include_rfc2369_headers: True
join_address: ant-join@example.com
@@ -63,13 +67,11 @@ All readable attributes for a list are available on a sub-resource.
reply_goes_to_list: no_munging
reply_to_address:
request_address: ant-request@example.com
- scheme: http
send_welcome_message: True
subject_prefix: [Ant]
subscription_policy: confirm
volume: 1
- web_host: lists.example.com
- welcome_message_uri: mailman:///welcome.txt
+ welcome_message_uri:
Changing the full configuration
@@ -109,7 +111,6 @@ When using ``PUT``, all writable attributes must be included.
... posting_pipeline='virgin',
... filter_content=True,
... first_strip_reply_to=True,
- ... goodbye_message_uri='mailman:///goodbye.txt',
... convert_html_to_plaintext=True,
... collapse_alternatives=False,
... reply_goes_to_list='point_to_list',
@@ -117,7 +118,6 @@ When using ``PUT``, all writable attributes must be included.
... send_welcome_message=False,
... subject_prefix='[ant]',
... subscription_policy='moderate',
- ... welcome_message_uri='mailman:///welcome.txt',
... default_member_action='hold',
... default_nonmember_action='discard',
... moderator_password='password',
@@ -162,8 +162,8 @@ These values are changed permanently.
display_name: Fnords
filter_content: True
first_strip_reply_to: True
+ footer_uri:
fqdn_listname: ant@example.com
- goodbye_message_uri: mailman:///goodbye.txt
...
include_rfc2369_headers: False
...
@@ -177,7 +177,6 @@ These values are changed permanently.
subject_prefix: [ant]
subscription_policy: moderate
...
- welcome_message_uri: mailman:///welcome.txt
Changing a partial configuration
diff --git a/src/mailman/rest/docs/systemconf.rst b/src/mailman/rest/docs/systemconf.rst
index fa8b7384b..385588077 100644
--- a/src/mailman/rest/docs/systemconf.rst
+++ b/src/mailman/rest/docs/systemconf.rst
@@ -12,6 +12,7 @@ get a list of all defined sections.
You can also get all the values for a particular section.
>>> dump_json('http://localhost:9001/3.0/system/configuration/mailman')
+ cache_life: 7d
default_language: en
email_commands_max_lines: 10
filtered_messages_are_preservable: no
diff --git a/src/mailman/rest/docs/templates.rst b/src/mailman/rest/docs/templates.rst
new file mode 100644
index 000000000..f5d6773dc
--- /dev/null
+++ b/src/mailman/rest/docs/templates.rst
@@ -0,0 +1,539 @@
+===========
+ Templates
+===========
+
+In Mailman 3.1 a new template system was introduced to allow for maximum
+flexibility in the format and content of messages sent by and through Mailman.
+For example, when a new member joins a list, a welcome message is sent to that
+member. The welcome message is created from a template found by a URL
+associated with a template name and a context.
+
+So if for example, you want to include links to pages on you website, you can
+create a custom template, make it available via download from a URL, and then
+associate that URL with a mailing list's welcome message. Some standard
+placeholders can be defined in the template, and these will be filled in by
+Mailman when the welcome message is sent.
+
+The URL itself can have placeholders, and this allows for additional
+flexibility when looking up the content.
+
+
+Examples
+========
+
+Let's say you have a mailing list::
+
+ >>> ant = create_list('ant@example.com')
+
+The standard welcome message doesn't have any links to it because by default
+Mailman doesn't know about any web user interface front-end. When Anne is
+subscribed to the mailing list, she sees this plain welcome message.
+
+ >>> anne = subscribe(ant, 'Anne')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Anne Person <aperson@example.com>
+ ...
+ <BLANKLINE>
+ Welcome to the "Ant" mailing list!
+ <BLANKLINE>
+ To post to this list, send your email to:
+ <BLANKLINE>
+ ant@example.com
+ <BLANKLINE>
+ You can make such adjustments via email by sending a message to:
+ <BLANKLINE>
+ ant-request@example.com
+ <BLANKLINE>
+ with the word 'help' in the subject or body (don't include the
+ quotes), and you will get back a message with instructions. You will
+ need your password to change your options, but for security purposes,
+ this email is not included here. There is also a button on your
+ options page that will send your current password to you.
+
+Let's say though that you wanted to provide a link to a Code of Conduct in the
+welcome message. You publish both the code of conduct and the welcome message
+pointing to the code on your website. Now you can tell the mailing list to
+use this welcome message instead of the default one.
+
+ >>> call_http('http://localhost:9001/3.1/lists/ant.example.com/uris', {
+ ... 'list:user:notice:welcome': 'http://localhost:8180/welcome_1.txt',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+The name of the template corresponding to the welcome message is
+`list:user:notice:welcome` and the location of your new welcome message text
+is at `http://localhost:8080/welcome_1.txt`.
+
+Now when a new member subscribes to the mailing list, they'll see the new
+welcome message.
+
+ >>> bill = subscribe(ant, 'Bill')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Bill Person <bperson@example.com>
+ ...
+ <BLANKLINE>
+ Welcome to the "Ant" mailing list!
+ <BLANKLINE>
+ To post to this list, send your email to:
+ <BLANKLINE>
+ ant@example.com
+ <BLANKLINE>
+ There is a Code of Conduct for this mailing list which you can view at
+ http://www.example.com/code-of-conduct.html
+
+It's even possible to require a username and password (Basic Auth) for
+retrieving the welcome message.
+
+ >>> call_http('http://localhost:9001/3.1/lists/ant.example.com/uris', {
+ ... 'list:user:notice:welcome': 'http://localhost:8180/welcome_2.txt',
+ ... 'username': 'anne',
+ ... 'password': 'is special',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+The username and password will be used to retrieve the welcome text.
+
+ >>> cris = subscribe(ant, 'Cris')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Cris Person <cperson@example.com>
+ ...
+ <BLANKLINE>
+ I'm glad you made it!
+
+The text is cached so subsequent uses don't necessarily need to hit the
+internet.
+
+ >>> dave = subscribe(ant, 'Dave')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Dave Person <dperson@example.com>
+ ...
+ <BLANKLINE>
+ I'm glad you made it!
+
+
+Template format
+===============
+
+Mailman expects the templates to be return as content type
+`text/plain; charset="UTF-8"`.
+
+Template URLs can be any of the following schemes:
+
+* `http://` - standard scheme supported by the requests_ library;
+* `https://` - standard scheme also supported by requests_;
+* `file:///` - any path on the local file system; UTF-8 contents by default;
+* `mailman:///` - a path defined within the Mailman source code tree. It is
+ not recommended that you use these; they are primarily provided for
+ `Mailman's internal use`_.
+
+Generally, if a template is not defined or not found, the empty string is
+used. IOW, a missing template does not cause an error, it simply causes the
+named template to be blank.
+
+
+URL placeholders
+================
+
+The URLs themselves can contain placeholders, and this can be used to provide
+even more flexibility in the way the template texts are retrieved. Two common
+placeholders include the List-ID and the mailing list's preferred language
+code.
+
+ >>> ant.preferred_language = 'fr'
+ >>> call_http('http://localhost:9001/3.1/lists/ant.example.com/uris', {
+ ... 'list:user:notice:welcome':
+ ... 'http://localhost:8180/$list_id/$language/welcome_3.txt',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+The next person to subscribe will get a French welcome message.
+
+ >>> dave = subscribe(ant, 'Elle')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="iso-8859-1"
+ Content-Transfer-Encoding: quoted-printable
+ Subject: =?iso-8859-1?q?Welcome_to_the_=22Ant=22_mailing_list?=
+ From: ant-request@example.com
+ To: Elle Person <eperson@example.com>
+ ...
+ <BLANKLINE>
+ Je suis heureux que vous pouvez nous rejoindre!
+
+Standard URL substitutions include:
+
+* `$list_id` - The mailing list's List-ID (`ant.example.com`)
+* `$listname` - The mailing list's fully qualified list name
+ (`ant@example.com`)
+* `$domain_name` - The mailing list's domain name (`example.com`)
+* `$language` - The language code for the mailing list's preferred language
+ (`fr`)
+
+
+Template contexts
+=================
+
+When Mailman is looking for a template, it always searches for it in up to
+three *contexts*, and you can set the template for any of these three
+contexts: a mailing list, a domain, the site.
+
+Most templates are searched first by the mailing list, then by domain, then by
+site. One notable exception is the ``domain:admin:notice:new-list`` template,
+which is sent when a new mailing list is created. Because (modulo any style
+default settings) there won't be a template for the newly created mailing
+list, this template is always searched for first in the domain, and then in
+the site.
+
+In fact, this illustrates a common naming scheme for templates. The
+colon-separated sections usually follow the form
+``<context>:<recipient>:<type>:<name>`` where ``context`` would be "domain" or
+"list, ``<recipient>`` would be "admin", "user", or "member", and ``<type>``
+can be "action" or "notice". This isn't a strict naming scheme, but it does
+give you some indication as to the use of the template. All template names
+used internally by Mailman are given below.
+
+You've already seen how the mailing list context works above. Let's look at
+the domain and site contexts next.
+
+
+Domain context
+--------------
+
+Let's say you want all mailing lists in a given domain to share exactly the
+same welcome message template. Remember that Mailman will insert
+substitutions into the templates themselves to customize them for each mailing
+list, so in general a single template can be shared by all mailing lists in
+the domain.
+
+The first thing to do is to set the URI for the welcome message in the domain
+to be shared.
+
+ >>> call_http('http://localhost:9001/3.1/domains/example.com/uris', {
+ ... 'list:user:notice:welcome':
+ ... 'http://localhost:8180/welcome_4.txt',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+And let's create a new mailing list in this domain.
+
+ >>> bee = create_list('bee@example.com')
+
+Now when Anne subscribes to the Bee mailing list, she will get this
+domain-wide welcome message.
+
+ >>> anne = subscribe(bee, 'Anne')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Bee" mailing list
+ From: bee-request@example.com
+ To: Anne Person <aperson@example.com>
+ ...
+ Welcome to the Bee list in the example.com domain.
+
+So far so good. What happens if Fred subscribes to the Ant mailing list?
+
+ >>> fred = subscribe(ant, 'Fred')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="iso-8859-1"
+ Content-Transfer-Encoding: quoted-printable
+ Subject: =?iso-8859-1?q?Welcome_to_the_=22Ant=22_mailing_list?=
+ From: ant-request@example.com
+ To: Fred Person <fperson@example.com>
+ ...
+ <BLANKLINE>
+ Je suis heureux que vous pouvez nous rejoindre!
+
+Okay, that's strange! Why did Fred get the French welcome message? It's
+because the mailing list context overrides the domain context! Similarly, a
+domain context overrides a site context. This allows you to provide generic
+templates to be used as a default, with specific overrides where necessary.
+
+Let's delete the Ant list's override.
+
+ >>> ant.preferred_language = 'en'
+ >>> call_http('http://localhost:9001/3.1/lists/ant.example.com/uris'
+ ... '/list:user:notice:welcome',
+ ... method='DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+Now when Gwen subscribes to the Ant list, she gets the domain's welcome
+message.
+
+ >>> gwen = subscribe(ant, 'Gwen')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Gwen Person <gperson@example.com>
+ ...
+ <BLANKLINE>
+ Welcome to the Ant list in the example.com domain.
+
+
+Site context
+------------
+
+Let's say we want the same welcome template for every mailing list on our
+Mailman installation. For this we use the site context.
+
+First, let's delete the domain context we set previously. Note that
+previously we used a `DELETE` method on the list's welcome template resource,
+but we could have also done this by PATCHing an empty string for the URI,
+which Mailman's REST API interprets as a deletion too. Let's use this
+approach to delete the domain welcome message.
+
+ >>> call_http('http://localhost:9001/3.1/domains/example.com/uris', {
+ ... 'list:user:notice:welcome': '',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+Now let's set a new welcome template URI for the site.
+
+ >>> call_http('http://localhost:9001/3.1/uris', {
+ ... 'list:user:notice:welcome':
+ ... 'http://localhost:8180/welcome_5.txt',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+Now Herb subscribes to both the Ant...
+
+ >>> herb = subscribe(ant, 'Herb')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Ant" mailing list
+ From: ant-request@example.com
+ To: Herb Person <hperson@example.com>
+ ...
+ <BLANKLINE>
+ Yay! You joined the ant@example.com mailing list.
+
+...and Bee mailing lists.
+
+ >>> herb = subscribe(bee, 'Herb')
+ >>> items = get_queue_messages('virgin')
+ >>> print(items[0].msg)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Welcome to the "Bee" mailing list
+ From: bee-request@example.com
+ To: Herb Person <hperson@example.com>
+ ...
+ <BLANKLINE>
+ Yay! You joined the bee@example.com mailing list.
+
+
+Templated texts
+===============
+
+All the texts that Mailman uses to create or decorate messages can be
+associated with a URL. Mailman looks up templates by name and downloads it
+via that URL. The retrieved text supports placeholders which are filled in by
+Mailman. There are a common set of placeholders most templates support:
+
+* ``listname`` - fully qualified list name (e.g. ``ant@example.com``)
+* ``list_id`` - the ``List-ID`` header (e.g. ``ant.example.com``)
+* ``display_name`` - the display name of the mailing list (e.g. ``Ant``)
+* ``short_listname`` - the local part of the list name (e.g. ``ant``)
+* ``domain`` - the domain name part of the list name (e.g. ``example.com``)
+* ``description`` - the mailing list's short description text
+* ``info`` - the mailing list's longer descriptive text
+* ``request_email`` - the email address for the ``-request`` alias
+* ``owner_email`` - the email address for the ``-owner`` alias
+* ``site_email`` - the email address to reach the owners of the site
+* ``language`` - the two letter language code for the list's preferred
+ language (e.g. ``en``, ``it``, ``fr``)
+
+Other template substitutions are described below the template name listed
+below. Here are all the supported template names:
+
+* ``domain:admin:notice:new-list``
+ Sent to the administrators of any newly created mailing list.
+
+* ``list:admin:action:post``
+ Sent to the list administrators when moderator approval for a posting is
+ required.
+
+ * ``subject`` - the original ``Subject`` of the message
+ * ``sender_email`` - the poster's email address
+ * ``reasons`` - some reasons why the post is being held for approval
+
+* ``list:admin:action:subscribe``
+ Sent to the list administrators when moderator approval for a subscription
+ request is required.
+
+ * ``member`` - display name and email address of the subscriber
+
+* ``list:admin:action:unsubscribe``
+ Sent to the list administrators when moderator approval for an
+ unsubscription request is required.
+
+ * ``member`` - display name and email address of the subscriber
+
+* ``list:admin:notice:subscribe``
+ Sent to the list administrators to notify them when a new member has
+ been subscribed.
+
+ * ``member`` - display name and email address of the subscriber
+
+* ``list:admin:notice:unrecognized``
+ Sent to the list administrators when a bounce message in an unrecognized
+ format has been received.
+
+* ``list:admin:notice:unsubscribe``
+ Sent to the list administrators to notify them when a member has been
+ unsubscribed.
+
+ * ``member`` - display name and email address of the subscriber
+
+* ``list:member:digest:footer``
+ The footer for a digest message.
+
+* ``list:member:digest:header``
+ The header for a digest message.
+
+* ``list:member:digest:masthead``
+ The digest "masthead"; i.e. a common introduction for all digest
+ messages.
+
+* ``list:member:regular:footer``
+ The footer for a regular (non-digest) message.
+
+ When personalized deliveries are enabled, these substitution variables are
+ also defined:
+
+ * ``member`` - display name and email address of the subscriber
+ * ``user_email`` - the email address of the recipient
+ * ``user_delivered_to`` - the case-preserved email address of the recipient
+ * ``user_language`` - the description of the user's preferred language
+ (e.g. "French", "English", "Italian")
+ * ``user_name`` - the recipient's display name if available
+
+* ``list:member:regular:header``
+ The header for a regular (non-digest) message.
+
+ When personalized deliveries are enabled, these substitution variables are
+ also defined:
+
+ * ``member`` - display name and email address of the subscriber
+ * ``user_email`` - the email address of the recipient
+ * ``user_delivered_to`` - the case-preserved email address of the recipient
+ * ``user_language`` - the description of the user's preferred language
+ (e.g. "French", "English", "Italian")
+ * ``user_name`` - the recipient's display name if available
+
+* ``list:user:action:confirm``
+ The message sent to subscribers when a subscription confirmation is
+ required.
+
+ * ``token`` - the unique confirmation token
+ * ``subject`` - the ``Subject`` heading for the confirmation email, which
+ includes the confirmation token
+ * ``confirm_email`` - the email address to send the confirmation response
+ to; this corresponds to the ``Reply-To`` header
+ * ``user_email`` - the email address being confirmed
+
+* ``list:user:notice:goodbye``
+ The notice sent to a member when they unsubscribe from a mailing list.
+
+* ``list:user:notice:hold``
+ The notice sent to a poster when their message is being held or moderator
+ approval.
+
+ * ``subject`` - the original ``Subject`` of the message
+ * ``sender_email`` - the poster's email address
+ * ``reasons`` - some reasons why the post is being held for approval
+
+* ``list:user:notice:no-more-today``
+ Sent to a user when the maximum number of autoresponses has been reached
+ for that day.
+
+ * ``sender_email`` - the email address of the poster
+ * ``count`` - the number of autoresponse messages sent to the user today
+
+* ``list:user:notice:post``
+ Notice sent to a poster when their message has been received by the
+ mailing list.
+
+ * ``subject`` - the ``Subject`` field of the received message
+
+* ``list:user:notice:probe``
+ A bounce probe sent to a member when their subscription has been disabled
+ due to bounces.
+
+ * ``sender_email`` - the email address of the bouncing member
+
+* ``list:user:notice:refuse``
+ Notice sent to a poster when their message has been rejected by the list's
+ moderator.
+
+ * ``request`` - the type of request being rejected
+ * ``reason`` - the reason for the rejection, as provided by the list's
+ moderators
+
+* ``list:user:notice:welcome``
+ The notice sent to a member when they are subscribed to the mailing list.
+
+ * ``user_name`` - the display name of the new member
+ * ``user_email`` - the email address of the new member
+
+
+.. _requests: http://docs.python-requests.org/en/master/
+.. _`Mailman's internal use`: https://gitlab.com/mailman/mailman/blob/master/src/mailman/utilities/i18n.py#L45