Mailman Client¶
The mailmanclient
library provides official Python bindings for the GNU
Mailman 3 REST API.
Requirements¶
mailmanclient
requires Python 3.5 or newer.
Documentation¶
A simple guide to using the library is available within this package, in the form of doctests. The manual is also available online at:
Project details¶
The project home page is:
You should report bugs at:
You can download the latest version of the package either from the Cheese Shop:
or from the GitLab page above. Of course you can also just install it with
pip
from the command line:
$ pip install mailmanclient
You can grab the latest development copy of the code using Git, from the Gitlab home page above. If you have Git installed, you can grab your own branch of the code like this:
$ git clone https://gitlab.com/mailman/mailmanclient.git
You may contact the developers via mailman-developers@python.org
Acknowledgements¶
Many thanks to Florian Fuchs for his contribution of an initial REST client. Also thanks to all the contributors of Mailman Client who have contributed code, raised issues or devoted their time in any capacity!
Contents¶
NEWS for mailmanclient¶
3.3.1 (2020-06-01)¶
- Held message moderation now supports an optional keyword,
reason
to specify the reason to reject the message. (Closes #49) - Fix a bug where missing
display_name
attribute withMalingList.subscribe
would subscribe the user with a display name of “None”. (Fixes #52) - Add
advertised
flag toMailingList
object. (See !115) MailingList.nonmembers
now usesroster/nonmembers
resource instead of thefind/
API for consistency.- Add
Client.get_nonmember
andMailingList.get_nonmember
to get a non-member by address. (Fixes #47)
3.3.0 (2019-09-03)¶
- Add a
mail_host
parameter toget_list_page
andfind_lists
to support filtering the response by a list domain. - URL encode values in URL which are url unsafe. (Closes #44)
- Add support to mass unsubscribe memebrs from a Mailing List. (Closes #43)
- Add support to set a user’s preferred address. (See !99)
- Add a new
tag
attribute to HeaderMatches and support to find a set - of matches based on tag.
- Add a new
3.2.2 (2019-02-09)¶
3.2.1 (2019-01-04)¶
- Add support for Python 3.7
- Add
description
as a property ofMailingList
. Initially, this was a part ofPreferences
object, which would mean an additional API call to get the description of a Mailing List. (Closes #35) MailingList.get_members
no longer requiresaddress
as a mandatory argument which allows searching for all memberships of of a particular role. Also,role
no longer has a default argument, so that we can search for all memberships of an address.
3.2.0 (2018-07-10)¶
Changes¶
- Add ‘.pc’ (patch directory) to list of ignored patterns when building the
- documentation with Sphinx.
- Mailinglist.add_owner and Mailinglist.add_moderator now accept an additional display_name argument that allows associating display names with these memberships.
- Add a new API
Client.find_lists
which allows filtering mailing lists - related to a subscriber. It optionally allows a role, which filters the lists that the address is subscribed to with that role.
- Add a new API
Backwards Incompatible Changes¶
- MailingList.owners and MailingList.moderators now returns a list of Member objects instead of a list of emails.
- Domain.owners now returns a list of User objects instead of just a dictionary of JSON response. (!63)
- Python 2.7 is no longer supported.
3.1.1 (2017-10-07)¶
- Python3 compatibility is fixed, mailmanclient is now compatible through Python2.7 - Python3.6
- Internal source code is now split into several class-specific modules as compared to previously a single giant _client module.
- All the RestObjects, like MailingList, are now exposed from the top level import.
- Old mailmanclient._client module is added back for compatibility with versions of Postorius that use some internal APIs.
3.1 (2017-05-25)¶
- Bug fixes.
- Align with Mailman 3.1 Core REST API.
- Python3 compatibility is broken because of a urllib bug.
1.0.1 (2015-11-14)¶
- Bugfix release.
1.0.0 (2015-04-17)¶
- Port to Python 3.4.
- Run test suite with tox.
- Use vcrpy for HTTP testing.
- Add list archiver access.
- Add subscription moderation
1.0.0a1 (2014-03-15)¶
- Initial release.
API Reference¶
-
class
mailmanclient.
Client
(baseurl, name=None, password=None)[source]¶ Access the Mailman REST API root.
Parameters: - baseurl – The base url to access the Mailman 3 REST API.
- name – The Basic Auth user name. If given, the password must also be given.
- password – The Basic Auth password. If given the name must also be given.
-
chains
¶ Get a list of all the Chains.
Returns: A list of all the chains in Core. Return type: List
-
configuration
¶ Get the system configuration.
Returns: All the system configuration. Return type: Dict[str, Configuration
]
-
create_domain
(mail_host, base_url=<object object>, description=None, owner=None, alias_domain=None)[source]¶ Create a new Domain.
Parameters: - mail_host (str) – The Mail host for the new domain. If you want foo@bar.com” as the address for your MailingList, use “bar.com” here.
- description (str) – A brief description for this Domain.
- owner (str) – Email address for the owner of this list.
- alias_domain (str) – Alias domain.
Returns: The created Domain.
Return type:
-
create_user
(email, password, display_name='')[source]¶ Create a new User.
Parameters: - email (str) – Email address for the new user.
- password (str) – Password for the new user.
- display_name (str) – An optional name for the new user.
Returns: The created user instance.
Return type:
-
delete_domain
(mail_host)[source]¶ Delete a Domain.
Parameters: mail_host (str) – The Mail host for the domain you want to delete.
-
delete_list
(fqdn_listname)[source]¶ Delete a MailingList.
Parameters: fqdn_listname (str) – Fully qualified name of the MailingList.
-
find_lists
(subscriber, role=None, count=50, page=1, mail_host=None)[source]¶ Given a subscriber and a role, return all the list they are subscribed to with given role.
If no role is specified all the related mailing lists are returned without duplicates, even though there can potentially be multiple memberships of a user in a single mailing list.
Parameters: - subscriber (str) – The address of the subscriber.
- role (str) – owner, moderator or subscriber.
- count (int) – Number of entries per-page (defaults to 50).
- page (int) – The page number to return (defaults to 1).
- mail_host (str) – Domain to filter results by.
Returns: A filtered list of mailing lists with given filters.
Return type: List[
MailingList
]
-
get_address
(address)[source]¶ Given an Email Address, return the Address object.
Parameters: address (str) – Email address. Returns: The Address object for given email address. Return type: Address
-
get_bans_page
(count=50, page=1)[source]¶ Get a list of all the bans with pagination.
Parameters: - count (int) – Number of entries per-page (defaults to 50).
- page (int) – The page number to return (defaults to 1).
Returns: Paginated list of banned addresses.
Return type: Page
ofBannedAddress
-
get_list
(fqdn_listname)[source]¶ Get a MailingList object.
Parameters: fqdn_listname (str) – Fully qualified name of the MailingList. Returns: The mailing list object of the given fqdn_listname. Return type: MailingList
-
get_list_page
(count=50, page=1, advertised=None, mail_host=None)[source]¶ Get a list of all MailingList with pagination.
Parameters: - count – Number of entries per-page (defaults to 50).
- page – The page number to return (defaults to 1).
- advertised – If marked True, returns all MailingLists including the ones that aren’t advertised.
- mail_host – Domain to filter results by.
-
get_lists
(advertised=False)[source]¶ Get a list of all the MailingLists.
Parameters: advertised (bool) – If marked True, returns all MailingLists including the ones that aren’t advertised. Returns: A list of mailing lists. Return type: List( MailingList
)
-
get_member
(fqdn_listname, subscriber_address)[source]¶ Get the Member object for a given MailingList and Subsciber’s Email Address.
Parameters: - fqdn_listname (str) – Fully qualified address for the MailingList.
- subscriber_address (str) – Email Address for the subscriber.
Returns: A member of a list.
Return type:
-
get_member_page
(count=50, page=1)[source]¶ Return a paginated list of Members.
Parameters: - count (int) – Number of items to return.
- page (int) – The page number.
Returns: Paginated lists of members.
Return type: Page
ofMember
.
-
get_nonmember
(fqdn_listname, nonmember_address)[source]¶ Get the Member object for a given MailingList and Non-member’s Email.
Parameters: - fqdn_listname (str) – Fully qualified address for the MailingList.
- subscriber_address (str) – Email Address for the non-member.
Returns: A member of a list.
Return type:
-
get_templates_page
(count=25, page=1)[source]¶ Get paginated site-context templates.
Returns: Paginated list of templates of site context. Return type: Page
ofTemplate
-
get_user
(address)[source]¶ Given an Email Address, return the User it belongs to.
Parameters: address (str) – Email Address of the User. Returns: The user instance that owns the address. Return type: User
-
get_user_page
(count=50, page=1)[source]¶ Get all the users with pagination.
Parameters: - count (int) – Number of entries per-page (defaults to 50).
- page (int) – The page number to return (defaults to 1).
Returns: Paginated list of users on Mailman.
Return type: Page
ofUser
-
lists
¶ Get a list of all MailingLists.
Returns: All the mailing lists. Return type: list( MailingList
)
-
pipelines
¶ Get a list of all Pipelines.
Returns: A list of all the pipelines in Core. Return type: List
-
preferences
¶ Get all default system Preferences.
Returns: System preferences. Return type: Preferences
-
queues
¶ Get a list of all Queues.
Returns: A list of all the queues in Core. Return type: List
-
set_template
(template_name, url, username=None, password=None)[source]¶ Set template in site-context.
Parameters: - template_name (str) – The template to set.
- url (str) – The URL to fetch the template from.
- username (str) – Username for access to the template.
- password (str) – Password for the
username
to access templates.
-
styles
¶ All the default styles in Mailman Core.
Returns: All the styles in Core. Return type: Styles
-
system
¶ Get the basic system information.
Returns: System information about Mailman Core Return type: Dict[str, str]
-
templates
¶ Get all site-context templates.
Returns: List of templates for the site context. Return type: TemplateList
-
class
mailmanclient.
Domain
(connection, url, data=None)[source]¶ -
-
base_url
¶
-
lists
¶
-
owners
¶
-
templates
¶
-
web_host
¶
-
-
class
mailmanclient.
MailingList
(connection, url, data=None)[source]¶ -
accept_message
(request_id)[source]¶ Shortcut for moderate_message.
Parameters: request_id (str) – The request_id of the held message.
-
archivers
¶
-
bans
¶
-
defer_message
(request_id)[source]¶ Shortcut for moderate_message.
Parameters: request_id (str) – The request_id of the held message.
-
discard_message
(request_id)[source]¶ Shortcut for moderate_message.
Parameters: request_id (str) – The request_id of the held message.
-
get_member
(email)[source]¶ Get a membership.
Parameters: address – The email address of the member for this list. Returns: A member proxy object.
-
get_nonmember
(email)[source]¶ Get a non-member of the list.
Parameters: address – The email address of the non-member for this list. Returns: A member proxy object.
-
header_matches
¶
-
held
¶ Return a list of dicts with held message information.
-
is_member
(address)[source]¶ Given an address, checks if the given address is subscribed to this mailing list.
-
is_moderator
(address)[source]¶ Given an address, checks if the given address is a moderator of this mailing list.
-
is_owner
(address)[source]¶ Given an address, checks if the given address is an owner of this mailing list.
-
is_owner_or_mod
(address)[source]¶ Given an address, checks if the given address is either a owner or a moderator of this list.
It is possible for them to be both owner and moderator.
-
mass_unsubscribe
(email_list)[source]¶ Unsubscribe a list of emails from a mailing list.
This function return a json of emails mapped to booleans based on whether they were unsubscribed or not, for whatever reasons
Parameters: email_list – list of emails to unsubscribe
-
members
¶
-
moderate_message
(request_id, action, comment=None)[source]¶ Moderate a held message.
Parameters: - request_id (Int.) – Id of the held message.
- action (String.) – Action to perform on held message.
- comment (str) – The reason for action, only supported for rejection.
-
moderate_request
(request_id, action)[source]¶ Moderate a subscription request.
Parameters: action (str.) – accept|reject|discard|defer
-
moderators
¶
-
nonmembers
¶
-
owners
¶
-
reject_message
(request_id, reason=None)[source]¶ Shortcut for moderate_message.
Parameters: - request_id (str) – The request_id of the held message.
- reason (str) – An optional reason for rejection of the message.
-
requests
¶ Return a list of dicts with subscription requests.
-
settings
¶
-
subscribe
(address, display_name=None, pre_verified=False, pre_confirmed=False, pre_approved=False)[source]¶ Subscribe an email address to a mailing list.
Parameters: - address (str) – Email address to subscribe to the list.
- display_name (str) – The real name of the new member.
- pre_verified (bool) – True if the address has been verified.
- pre_confirmed (bool) – True if membership has been approved by the user.
- pre_approved (bool) – True if membership is moderator-approved.
Returns: A member proxy object.
-
templates
¶
-
-
class
mailmanclient.
ListArchivers
(connection, url, mlist)[source]¶ Represents the activation status for each site-wide available archiver for a given list.
-
class
mailmanclient.
Bans
(connection, url, data=None, mlist=None)[source]¶ The list of banned addresses from a mailing-list or from the whole site.
-
class
mailmanclient.
HeaderMatches
(connection, url, mlist)[source]¶ The list of header matches for a mailing-list.
-
add
(header, pattern, action=None, tag=None)[source]¶ Add a new HeaderMatch rule to the MailingList.
Parameters: - header (str) – The header to consider.
- pattern (str) – The regular expression to use for filtering.
- action (str) – The action to take when the header matches the pattern. This can be ‘accept’, ‘discard’, ‘reject’, or ‘hold’.
-
find
(header=None, tag=None, action=None)[source]¶ Find a set of HeaderMatch rules.
Parameters: - header (str) – The header to consider.
- tag (str) – The tag associated with header.
- action (str) – The action to take when the header matches the pattern. This can be ‘accept’, ‘discard’, ‘reject’, or ‘hold’.
-
-
class
mailmanclient.
User
(connection, url, data=None)[source]¶ -
add_address
(email, absorb_existing=False)[source]¶ Adds another email adress to the user record and returns an _Address object.
Parameters: - email (str.) – The address to add
- absorb_existing (bool.) – set this to True if you want to add the address even if it already exists. It will import the existing user into the current one, not overwriting any previously set value.
-
addresses
¶
-
preferred_address
¶ Preferred address of a User.
New in version 3.2.3.
Returns: Address or None.
-
subscription_list_ids
¶
-
subscriptions
¶
-
Developing MailmanClient¶
Running Tests¶
The test suite is run with the tox tool, which allows it to be run against multiple versions of Python. The tests are discovered and run using pytest.
To run the test suite, run:
$ tox
To run tests for only one version of Python, you can run:
$ tox -e py36
pytest
starts Mailman Core using pytest-services
plugin and
automatically manages it’s start and stop cycle for every module.
Note
Previously, we used vcrpy
and pytest-vcr
packages to manage
recorded tapes for interaction with Mailman Core. That was replaced
with pytest-services
plugin, which instead start Core for every
test.
Example Usage¶
This is the official Python bindings for the GNU Mailman REST API. In order to talk to Mailman, the engine’s REST server must be running. You begin by instantiating a client object to access the root of the REST hierarchy, providing it the base URL, user name and password (for Basic Auth).
>>> from mailmanclient import Client
>>> client = Client('http://localhost:9001/3.1', 'restadmin', 'restpass')
Note
Please note that port ‘9001’ is used above, since mailman’s test server runs on port 9001. In production Mailman’s REST API usually listens on port 8001.
We can retrieve basic information about the server.
>>> dump(client.system)
api_version: 3.1
http_etag: "..."
mailman_version: GNU Mailman ... (...)
python_version: ...
self_link: http://localhost:9001/3.1/system/versions
To start with, there are no known mailing lists.
>>> client.lists
[]
Domains¶
Before new mailing lists can be added, the domain that the list will live in must be added. By default, there are no known domains.
>>> client.domains
[]
It’s easy to create a new domain; when you do, a proxy object for that domain is returned.
>>> example_dot_com = client.create_domain('example.com')
>>> print(example_dot_com.description)
None
>>> print(example_dot_com.mail_host)
example.com
>>> print(example_dot_com.alias_domain)
None
A domain can have an alias_domain attribute to help with some unusual Postfix configurations.
>>> example_dot_edu = client.create_domain('example.edu',
... alias_domain='x.example.edu')
>>> print(example_dot_edu.mail_host)
example.edu
>>> print(example_dot_edu.alias_domain)
x.example.edu
You can also get an existing domain independently using its mail host.
>>> example = client.get_domain('example.com')
>>> print(example.mail_host)
example.com
After creating a few more domains, we can print the list of all domains.
>>> example_net = client.create_domain('example.net')
>>> example_org = client.create_domain('example.org')
>>> print(example_org.mail_host)
example.org
>>> for domain in client.domains:
... print(domain.mail_host)
example.com
example.edu
example.net
example.org
Also, domain can be deleted.
>>> example_org.delete()
>>> for domain in client.domains:
... print(domain.mail_host)
example.com
example.edu
example.net
Mailing lists¶
Once you have a domain, you can create mailing lists in that domain.
>>> test_one = example.create_list('test-1')
>>> print(test_one.fqdn_listname)
test-1@example.com
>>> print(test_one.mail_host)
example.com
>>> print(test_one.list_name)
test-1
>>> print(test_one.display_name)
Test-1
You can create a mailing list with a specific list style.
>>> test_two = example.create_list('test-announce', style_name='legacy-announce')
>>> print(test_two.fqdn_listname)
test-announce@example.com
You can retrieve a list of known mailing list styles along with the default one.
>>> styles = client.styles
>>> from operator import itemgetter
>>> for style in sorted(styles['styles'], key=itemgetter('name')):
... print('{0}: {1}'.format(style['name'], style['description']))
legacy-announce: Announce only mailing list style.
legacy-default: Ordinary discussion mailing list style.
private-default: Discussion mailing list style with private archives.
>>> print(styles['default'])
legacy-default
You can also retrieve the mailing list after the fact.
>>> my_list = client.get_list('test-1@example.com')
>>> print(my_list.fqdn_listname)
test-1@example.com
And you can print all the known mailing lists.
>>> print(example.create_list('test-2').fqdn_listname)
test-2@example.com
>>> domain = client.get_domain('example.net')
>>> print(domain.create_list('test-3').fqdn_listname)
test-3@example.net
>>> print(example.create_list('test-3').fqdn_listname)
test-3@example.com
>>> for mlist in client.lists:
... print(mlist.fqdn_listname)
test-1@example.com
test-2@example.com
test-3@example.com
test-3@example.net
test-announce@example.com
You can also select advertised lists only.
>>> my_list.settings['advertised'] = False
>>> my_list.settings.save()
>>> for mlist in client.get_lists(advertised=True):
... print(mlist.fqdn_listname)
test-2@example.com
test-3@example.com
test-3@example.net
test-announce@example.com
List results can be retrieved as pages:
>>> page = client.get_list_page(count=2, page=1)
>>> page.nr
1
>>> len(page)
2
>>> page.total_size
5
>>> for m_list in page:
... print(m_list.fqdn_listname)
test-1@example.com
test-2@example.com
>>> page = page.next
>>> page.nr
2
>>> for m_list in page:
... print(m_list.fqdn_listname)
test-3@example.com
test-3@example.net
Pages can also use the advertised filter:
>>> page = client.get_list_page(count=2, page=1, advertised=True)
>>> for m_list in page:
... print(m_list.fqdn_listname)
test-2@example.com
test-3@example.com
Pages can also limit the results by domain:
>>> page = client.get_list_page(mail_host='example.net')
>>> for m_list in page:
... print(m_list.fqdn_listname)
test-3@example.net
You can also use the domain object if you only want to know all lists for a specific domain without pagination.
>>> for mlist in example.lists:
... print(mlist.fqdn_listname)
test-1@example.com
test-2@example.com
test-3@example.com
test-announce@example.com
It is also possible to display only advertised lists when using the domain.
>>> for mlist in example.get_lists(advertised=True):
... print(mlist.fqdn_listname)
test-2@example.com
test-3@example.com
test-announce@example.com
>>> for mlist in example.get_list_page(count=2, page=1, advertised=True):
... print(mlist.fqdn_listname)
test-2@example.com
test-3@example.com
You can use a list instance to delete the list.
>>> test_three = client.get_list('test-3@example.net')
>>> test_three.delete()
You can also delete a list using the client instance’s delete_list method.
>>> client.delete_list('test-3@example.com')
>>> for mlist in client.lists:
... print(mlist.fqdn_listname)
test-1@example.com
test-2@example.com
test-announce@example.com
Membership¶
Email addresses can subscribe to existing mailing lists, becoming members of that list. The address is a unique id for a specific user in the system, and a member is a user that is subscribed to a mailing list. Email addresses need not be pre-registered, though the auto-registered user will be unique for each email address.
The system starts out with no members.
>>> client.members
[]
New members can be easily added; users are automatically registered.
>>> test_two = client.get_list('test-2@example.com')
>>> print(test_two.settings['subscription_policy'])
confirm
Email addresses need to be verified first, so if we try to subscribe a user, we get a response with a token:
>>> data = test_one.subscribe('unverified@example.com', 'Unverified')
>>> data['token'] is not None
True
>>> print(data['token_owner'])
subscriber
If we know the email address to be valid, we can set the
pre_verified
flag. However, the list’s subscription policy is
“confirm”, so if we try to subscribe a user, we will also get a token
back:
>>> data = test_one.subscribe('unconfirmed@example.com',
... 'Unconfirmed',
... pre_verified=True)
>>> data['token'] is not None
True
>>> print(data['token_owner'])
subscriber
If we know the user originated the subscription (for example if she or
he has been authenticated elsewhere), we can set the pre_confirmed
flag.
The pre_approved
flag is used for lists that require moderator
approval and should only be used if the subscription is initiated by a
moderator or admin.
>>> print(test_one.subscribe('anna@example.com', 'Anna',
... pre_verified=True,
... pre_confirmed=True))
Member "anna@example.com" on "test-1.example.com"
>>> print(test_one.subscribe('bill@example.com', 'Bill',
... pre_verified=True,
... pre_confirmed=True))
Member "bill@example.com" on "test-1.example.com"
>>> print(test_two.subscribe('anna@example.com',
... pre_verified=True,
... pre_confirmed=True))
Member "anna@example.com" on "test-2.example.com"
>>> print(test_two.subscribe('cris@example.com', 'Cris',
... pre_verified=True,
... pre_confirmed=True))
Member "cris@example.com" on "test-2.example.com"
We can retrieve all known memberships. These are sorted first by mailing list name, then by email address.
>>> for member in client.members:
... print(member)
Member "anna@example.com" on "test-1.example.com"
Member "bill@example.com" on "test-1.example.com"
Member "anna@example.com" on "test-2.example.com"
Member "cris@example.com" on "test-2.example.com"
We can also view the memberships for a single mailing list.
>>> for member in test_one.members:
... print(member)
Member "anna@example.com" on "test-1.example.com"
Member "bill@example.com" on "test-1.example.com"
Membership may have a name associated, this depends on whether the member Address
or User
has a display_name
attribute.
>>> for member in test_one.members:
... print(member.display_name)
Anna
Bill
Membership lists can be paginated, to recieve only a part of the result.
>>> page = client.get_member_page(count=2, page=1)
>>> page.nr
1
>>> page.total_size
4
>>> for member in page:
... print(member)
Member "anna@example.com" on "test-1.example.com"
Member "bill@example.com" on "test-1.example.com"
>>> page = page.next
>>> page.nr
2
>>> for member in page:
... print(member)
Member "anna@example.com" on "test-2.example.com"
Member "cris@example.com" on "test-2.example.com"
>>> page = test_one.get_member_page(count=1, page=1)
>>> page.nr
1
>>> page.total_size
2
>>> for member in page:
... print(member)
Member "anna@example.com" on "test-1.example.com"
>>> page = page.next
>>> page.nr
2
>>> page.total_size
2
>>> for member in page:
... print(member)
Member "bill@example.com" on "test-1.example.com"
We can get a single membership too.
>>> cris_test_two = test_two.get_member('cris@example.com')
>>> print(cris_test_two)
Member "cris@example.com" on "test-2.example.com"
>>> print(cris_test_two.role)
member
>>> print(cris_test_two.display_name)
Cris
A membership can also be retrieved without instantiating the list object first:
>>> print(client.get_member('test-2@example.com', 'cris@example.com'))
Member "cris@example.com" on "test-2.example.com"
A membership has preferences.
>>> prefs = cris_test_two.preferences
>>> print(prefs['delivery_mode'])
None
>>> print(prefs['acknowledge_posts'])
None
>>> print(prefs['delivery_status'])
None
>>> print(prefs['hide_address'])
None
>>> print(prefs['preferred_language'])
None
>>> print(prefs['receive_list_copy'])
None
>>> print(prefs['receive_own_postings'])
None
The membership object’s user
attribute will return a User object:
>>> cris_u = cris_test_two.user
>>> print(cris_u.display_name, cris_u.user_id)
Cris ...
If you use an address which is not a member of test_two ValueError is raised:
>>> test_two.unsubscribe('nomember@example.com')
Traceback (most recent call last):
...
ValueError: nomember@example.com is not a member address of test-2@example.com
After a while, Anna decides to unsubscribe from the Test One mailing list, though she keeps her Test Two membership active.
>>> import time
>>> time.sleep(2)
>>> test_one.unsubscribe('anna@example.com')
>>> for member in client.members:
... print(member)
Member "bill@example.com" on "test-1.example.com"
Member "anna@example.com" on "test-2.example.com"
Member "cris@example.com" on "test-2.example.com"
A little later, Cris decides to unsubscribe from the Test Two mailing list.
>>> cris_test_two.unsubscribe()
>>> for member in client.members:
... print(member)
Member "bill@example.com" on "test-1.example.com"
Member "anna@example.com" on "test-2.example.com"
If you try to unsubscribe an address which is not a member address ValueError is raised:
>>> test_one.unsubscribe('nomember@example.com')
Traceback (most recent call last):
...
ValueError: nomember@example.com is not a member address of test-1@example.com
If we want to mass unsubscribe users.
>>> print(test_one.subscribe('jack@example.com', 'Jack',
... pre_verified=True,
... pre_confirmed=True))
Member "jack@example.com" on "test-1.example.com"
>>> print(test_one.subscribe('jill@example.com', 'Jill',
... pre_verified=True,
... pre_confirmed=True))
Member "jill@example.com" on "test-1.example.com"
>>> print(test_one.subscribe('hans@example.com', 'Hans',
... pre_verified=True,
... pre_confirmed=True))
Member "hans@example.com" on "test-1.example.com"
>>> email_list = ['jack@example.com','hans@example.com','jill@example.com','bully@example.com']
>>> ();test_one.mass_unsubscribe(email_list);() # doctest: +ELLIPSIS
(...)
>>> for member in test_one.members:
... print(member)
Member "bill@example.com" on "test-1.example.com"
Non-Members¶
When someone attempts to post to a list but is not a member, then they are listed as a “non-member” of that list so that a moderator can choose how to handle their messages going forward. In some cases, one might wish to accept or reject their future messages automatically. Just like with regular members, they are given a unique id.
The list starts out with no nonmembers.
>>> test_one.nonmembers
[]
When someone tries to send a message to the list and they are not a subscriber, they get added to the nonmember list.
Users¶
Users are people with one or more list memberships. To get a list of all users, access the clients user property.
>>> for user in client.users:
... print(user.display_name)
Unverified
Unconfirmed
Anna
Bill
Cris
Jack
Jill
Hans
The list of users can also be paginated:
>>> page = client.get_user_page(count=4, page=1)
>>> page.nr
1
>>> page.total_size
8
>>> for user in page:
... print(user.display_name)
Unverified
Unconfirmed
Anna
Bill
You can get the next or previous pages without calling get_userpage
again.
>>> page = page.next
>>> page.nr
2
>>> for user in page:
... print(user.display_name)
Cris
Jack
Jill
Hans
>>> page = page.previous
>>> page.nr
1
>>> for user in page:
... print(user.display_name)
Unverified
Unconfirmed
Anna
Bill
A single user can be retrieved using their email address.
>>> cris = client.get_user('cris@example.com')
>>> print(cris.display_name)
Cris
Every user has a list of one or more addresses.
>>> for address in cris.addresses:
... print(address)
... print(address.display_name)
... print(address.registered_on)
cris@example.com
Cris
...
Multiple addresses can be assigned to a user record:
>>> print(cris.add_address('cris.person@example.org'))
cris.person@example.org
>>> print(client.get_address('cris.person@example.org'))
cris.person@example.org
>>> for address in cris.addresses:
... print(address)
cris.person@example.org
cris@example.com
Trying to add an existing address will raise an error:
>>> dana = client.create_user(email='dana@example.org',
... password='somepass',
... display_name='Dana')
>>> print(dana.display_name)
Dana
>>> cris.add_address('dana@example.org') # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
HTTPError: HTTP Error 400: Address already exists
This can be overridden by using the absorb_existing
flag:
>>> print(cris.add_address('dana@example.org', absorb_existing=True))
dana@example.org
The user Chris will then be merged with Dana, acquiring all its subscriptions and preferences. In case of conflict, Chris’ original preferences will prevail.
>>> for address in cris.addresses:
... print(address)
cris.person@example.org
cris@example.com
dana@example.org
Users can have one preferred address, which they can use for subscriptions. By default, a User has no preferred address.
>>> print(cris.preferred_address)
None
A User can have a preferred address, but before that, the address needs to be verified:
>>> address = client.get_address('cris.person@example.org')
>>> address.verify()
>>> print(address.verified)
True
>>> cris.preferred_address = 'cris.person@example.org'
>>> print(cris.preferred_address)
cris.person@example.org
A User can change their preferred address.
>>> cris.preferred_address = 'cris@example.com'
>>> print(cris.preferred_address)
cris@example.com
A User can also unset their preferred address by setting it to None
.
>>> cris.preferred_address = None
>>> print(cris.preferred_address)
None
Addresses¶
Addresses can be accessed directly:
>>> address = client.get_address('dana@example.org')
>>> print(address)
dana@example.org
>>> print(address.display_name)
Dana
The address has not been verified:
>>> print(address.verified)
False
But that can be done via the address object:
>>> address.verify()
>>> print(address.verified)
True
It can also be unverified:
>>> address.unverify()
>>> print(address.verified)
False
Addresses can be deleted by calling their delete()
method or by removing
them from their user’s addresses
list:
>>> cris.addresses.remove('dana@example.org')
>>> for address in cris.addresses:
... print(address)
cris.person@example.org
cris@example.com
- Users can be added using
create_user
. The display_name is optional: >>> ler = client.create_user(email='ler@primus.org', ... password='somepass', ... display_name='Ler') >>> print(ler.display_name) Ler >>> ler = client.get_user('ler@primus.org') >>> print(ler.password) $... >>> print(ler.display_name) Ler
User attributes can be changed through assignment, but you need to call the
object’s save
method to store the changes in the mailman core database.
>>> ler.display_name = 'Sir Ler'
>>> ler.save()
>>> ler = client.get_user('ler@primus.org')
>>> print(ler.display_name)
Sir Ler
Passwords can be changed as well:
>>> old_pwd = ler.password
>>> ler.password = 'easy'
>>> old_pwd == ler.password
True
>>> ler.save()
>>> old_pwd == ler.password
False
User Subscriptions¶
A User’s subscriptions can be access through their subscriptions
property.
>>> bill = client.get_user('bill@example.com')
>>> for subscription in bill.subscriptions:
... print(subscription)
Member "bill@example.com" on "test-1.example.com"
If all you need are the list ids of all mailing lists a user is subscribed to,
you can use the subscription_list_ids
property.
>>> for list_id in bill.subscription_list_ids:
... print(list_id)
test-1.example.com
List Settings¶
We can get all list settings via a lists settings attribute. A proxy object for the settings is returned which behaves much like a dictionary.
>>> settings = test_one.settings
>>> for attr in sorted(settings):
... print(attr + ': ' + str(settings[attr]))
accept_these_nonmembers: []
acceptable_aliases: []
...
volume: 1
>>> print(settings['display_name'])
Test-1
We can access all valid list settings as attributes.
>>> print(settings['fqdn_listname'])
test-1@example.com
>>> print(settings['description'])
<BLANKLINE>
>>> settings['description'] = 'A very meaningful description.'
>>> settings['display_name'] = 'Test Numero Uno'
>>> settings.save()
>>> settings_new = test_one.settings
>>> print(settings_new['description'])
A very meaningful description.
>>> print(settings_new['display_name'])
Test Numero Uno
The settings object also supports the get method of usual Python dictionaries:
>>> print(settings_new.get('OhNoIForgotTheKey',
... 'HowGoodIPlacedOneUnderTheDoormat'))
HowGoodIPlacedOneUnderTheDoormat
Preferences¶
Preferences can be accessed and set for users, members and addresses.
By default, preferences are not set and fall back to the global system preferences. They’re read-only and can be accessed through the client object.
>>> global_prefs = client.preferences
>>> print(global_prefs['acknowledge_posts'])
False
>>> print(global_prefs['delivery_mode'])
regular
>>> print(global_prefs['delivery_status'])
enabled
>>> print(global_prefs['hide_address'])
True
>>> print(global_prefs['preferred_language'])
en
>>> print(global_prefs['receive_list_copy'])
True
>>> print(global_prefs['receive_own_postings'])
True
Preferences can be set, but you have to call save
to make your changes
permanent.
>>> prefs = test_two.get_member('anna@example.com').preferences
>>> prefs['delivery_status'] = 'by_user'
>>> prefs.save()
>>> prefs = test_two.get_member('anna@example.com').preferences
>>> print(prefs['delivery_status'])
by_user
Pipelines and Chains¶
The available pipelines and chains can also be retrieved:
>>> pipelines = client.pipelines['pipelines']
>>> for pipeline in pipelines:
... print(pipeline)
default-owner-pipeline
default-posting-pipeline
virgin
>>> chains = client.chains['chains']
>>> for chain in chains:
... print(chain)
accept
default-owner-chain
default-posting-chain
discard
dmarc
header-match
hold
moderation
reject
Owners and Moderators¶
Owners and moderators are properties of the list object.
>>> test_one.owners
[]
>>> test_one.moderators
[]
Owners can be added via the add_owner
method and they can have an optional
display_name
associated like other members
:
>>> test_one.add_owner('foo@example.com', display_name='Foo')
>>> for owner in test_one.owners:
... print(owner.email)
foo@example.com
The owner of the list not automatically added as a member:
>>> for m in test_one.members:
... print(m)
Member "bill@example.com" on "test-1.example.com"
Moderators can be added similarly:
>>> test_one.add_moderator('bar@example.com', display_name='Bar')
>>> for moderator in test_one.moderators:
... print(moderator.email)
bar@example.com
Moderators are also not automatically added as members:
>>> for m in test_one.members:
... print(m)
Member "bill@example.com" on "test-1.example.com"
Members and owners/moderators are separate entries in in the general members list:
>>> print(test_one.subscribe('bar@example.com', 'Bar',
... pre_verified=True,
... pre_confirmed=True))
Member "bar@example.com" on "test-1.example.com"
>>> test_four_net = example_net.create_list('test-4')
>>> test_four_net.add_owner('foo@example.com', display_name='Foo')
>>> for member in client.members:
... print('%s: %s' % (member, member.role))
Member "foo@example.com" on "test-1.example.com": owner
Member "bar@example.com" on "test-1.example.com": moderator
Member "bar@example.com" on "test-1.example.com": member
Member "bill@example.com" on "test-1.example.com": member
Member "anna@example.com" on "test-2.example.com": member
Member "foo@example.com" on "test-4.example.net": owner
You can find the lists that a user is a member, moderator, or owner of:
>>> lists = client.find_lists('bill@example.com', 'member')
>>> for m_list in lists:
... print(m_list.fqdn_listname)
test-1@example.com
>>> lists = client.find_lists('bar@example.com', 'moderator')
>>> for m_list in lists:
... print(m_list.fqdn_listname)
test-1@example.com
>>> lists = client.find_lists('foo@example.com', 'owner')
>>> for m_list in lists:
... print(m_list.fqdn_listname)
test-1@example.com
test-4@example.net
You can also filter those results by domain:
>>> lists = client.find_lists('foo@example.com', 'owner',
... mail_host='example.net')
>>> for m_list in lists:
... print(m_list.fqdn_listname)
test-4@example.net
Both owners and moderators can be removed:
>>> test_one.remove_owner('foo@example.com') >>> test_one.owners []test_one.remove_moderator(‘bar@example.com’) test_one.moderators []
Moderation¶
Subscription Moderation¶
Subscription requests can be accessed through the list object’s request property. So let’s create a non-open list first.
>>> confirm_first = example_dot_com.create_list('confirm-first')
>>> settings = confirm_first.settings
>>> settings['subscription_policy'] = 'moderate'
>>> settings.save()
>>> confirm_first = client.get_list('confirm-first.example.com')
>>> print(confirm_first.settings['subscription_policy'])
moderate
Initially there are no requests, so let’s to subscribe someone to the list. We’ll get a token back.
>>> confirm_first.requests
[]
>>> data = confirm_first.subscribe('groucho@example.com',
... pre_verified=True,
... pre_confirmed=True)
>>> print(data['token_owner'])
moderator
Now the request shows up in the list of requests:
>>> import time; time.sleep(5)
>>> len(confirm_first.requests)
1
>>> request_1 = confirm_first.requests[0]
>>> print(request_1['email'])
groucho@example.com
>>> print (request_1['token'] is not None)
True
>>> print(request_1['token_owner'])
moderator
>>> print(request_1['request_date'] is not None)
True
>>> print(request_1['list_id'])
confirm-first.example.com
Subscription requests can be accepted, deferred, rejected or discarded using the request token.
>>> data = confirm_first.subscribe('harpo@example.com',
... pre_verified=True,
... pre_confirmed=True)
>>> data = confirm_first.subscribe('zeppo@example.com',
... pre_verified=True,
... pre_confirmed=True)
>>> len(confirm_first.requests)
3
Let’s accept Groucho:
>>> response = confirm_first.moderate_request(request_1['token'], 'accept')
>>> len(confirm_first.requests)
2
>>> request_2 = confirm_first.requests[0]
>>> print(request_2['email'])
harpo@example.com
>>> request_3 = confirm_first.requests[1]
>>> print(request_3['email'])
zeppo@example.com
Let’s reject Harpo:
>>> response = confirm_first.moderate_request(request_2['token'], 'reject')
>>> len(confirm_first.requests)
1
Let’s discard Zeppo’s request:
>>> response = confirm_first.moderate_request(request_3['token'], 'discard')
>>> len(confirm_first.requests)
0
Message Moderation¶
By injecting a message by a non-member into the incoming queue, we can simulate a message being held for moderator approval.
>>> msg = """From: nomember@example.com
... To: test-1@example.com
... Subject: Something
... Message-ID: <moderated_01>
...
... Some text.
...
... """
>>> inq = client.queues['in']
>>> inq.inject('test-1.example.com', msg)
Now wait until the message has been processed.
>>> while True:
... if len(inq.files) == 0:
... break
... time.sleep(0.1)
It might take a few moments for the message to show up in the moderation queue.
>>> while True:
... all_held = test_one.held
... if len(all_held) > 0:
... break
... time.sleep(0.1)
Messages held for moderation can be listed on a per list basis.
>>> print(all_held[0].request_id)
1
A held message can be retrieved by ID, and have attributes:
>>> heldmsg = test_one.get_held_message(1)
>>> print(heldmsg.subject)
Something
>>> print(heldmsg.reason)
The message is not from a list member
>>> print(heldmsg.sender)
nomember@example.com
>>> 'Message-ID: <moderated_01>' in heldmsg.msg
True
A moderation action can be taken on them using the list methods or the held message’s methods.
>>> print(test_one.defer_message(heldmsg.request_id).status_code)
204
>>> len(test_one.held)
1
>>> print(heldmsg.discard().status_code)
204
>>> len(test_one.held)
0
Member moderation¶
Each member or non-member can have a specific moderation action. It is set using the ‘moderation_action’ property:
>>> bill_member = test_one.get_member('bill@example.com')
>>> print(bill_member.moderation_action)
None
>>> bill_member.moderation_action = 'hold'
>>> bill_member.save()
>>> print(test_one.get_member('bill@example.com').moderation_action)
hold
Banning addresses¶
A ban list is a list of email addresses that are not allowed to subscribe to a mailing-list. There are two types of ban lists: each mailing-list has its ban list, and there is a site-wide list. Addresses on the site-wide list are prevented from subscribing to every mailing-list on the server.
To view the site-wide ban list, use the bans property:
>>> list(client.bans)
[]
You can use the add method on the ban list to ban an email address:
>>> banned_anna = client.bans.add('anna@example.com')
>>> print(banned_anna)
anna@example.com
>>> 'anna@example.com' in client.bans
True
>>> print(client.bans.add('bill@example.com'))
bill@example.com
>>> for addr in list(client.bans):
... print(addr)
anna@example.com
bill@example.com
The list of banned addresses can be paginated using the get_bans_page()
method:
>>> for addr in list(client.get_bans_page(count=1, page=1)):
... print(addr)
anna@example.com
>>> for addr in list(client.get_bans_page(count=1, page=2)):
... print(addr)
bill@example.com
You can use the delete()
method on a banned address to unban it, or the
remove()
method on the ban list:
>>> banned_anna.delete()
>>> 'anna@example.com' in client.bans
False
>>> for addr in list(client.bans):
... print(addr)
bill@example.com
>>> client.bans.remove('bill@example.com')
>>> 'bill@example.com' in client.bans
False
>>> print(list(client.bans))
[]
The mailing-list-specific ban lists work in the same way:
>>> print(list(test_one.bans))
[]
>>> banned_anna = test_one.bans.add('anna@example.com')
>>> 'anna@example.com' in test_one.bans
True
>>> print(test_one.bans.add('bill@example.com'))
bill@example.com
>>> for addr in list(test_one.bans):
... print(addr)
anna@example.com
bill@example.com
>>> for addr in list(test_one.get_bans_page(count=1, page=1)):
... print(addr)
anna@example.com
>>> for addr in list(test_one.get_bans_page(count=1, page=2)):
... print(addr)
bill@example.com
>>> banned_anna.delete()
>>> 'anna@example.com' in test_one.bans
False
>>> test_one.bans.remove('bill@example.com')
>>> print(list(test_one.bans))
[]
Archivers¶
Each list object has an archivers
attribute.
>>> archivers = test_one.archivers
>>> print(archivers)
Archivers on test-1.example.com
The activation status of each available archiver can be accessed like a key in a dictionary.
>>> archivers = test_one.archivers
>>> for archiver in sorted(archivers.keys()):
... print('{0}: {1}'.format(archiver, archivers[archiver]))
mail-archive: True
mhonarc: True
prototype: True
>>> archivers['mail-archive']
True
>>> archivers['mhonarc']
True
They can also be set like items in dictionary.
>>> archivers['mail-archive'] = False
>>> archivers['mhonarc'] = False
So if we get a new archivers
object from the API (by accessing the
list’s archiver attribute again), we can see that the archiver stati
have now been set.
>>> archivers = test_one.archivers
>>> archivers['mail-archive']
False
>>> archivers['mhonarc']
False
Header matches¶
Header matches are filtering rules that apply to messages sent to a mailing
list. They match a header to a pattern using a regular expression, and matching
patterns can trigger specific moderation actions. They are accessible via the
mailing list’s header_matches
attribute, which behaves like a list.
>>> header_matches = test_one.header_matches
>>> print(header_matches)
Header matches for "test-1.example.com"
>>> len(header_matches)
0
Header matches can be added using the add()
method. The arguments are:
the header to consider (
str
). Il will be lower-cased.the regular expression to use for filtering (
str
)the action to take when the header matches the pattern. This can be
'accept'
,'discard'
,'reject'
, or'hold'
.the tag (
str
) to group a set of header matches.>>> print(header_matches.add('Subject', '^test: ', 'discard', 'sometag')) Header match on "subject" >>> print(header_matches) Header matches for "test-1.example.com" >>> len(header_matches) 1 >>> for hm in list(header_matches): ... print(hm) Header match on "subject"
Header matches can be filtered using .find()
method to query a set
of HeaderMatches:
>>> header_matches.find(tag='sometag')
[<HeaderMatch on 'subject'>]
You can delete a header match by deleting it from the header_matches
collection.
>>> del header_matches[0]
>>> len(header_matches)
0
You can also delete a header match using its delete()
method, but be aware
that the collection will not automatically be updated. Get a new collection
from the list’s header_matches
attribute to see the change.
>>> print(header_matches.add('Subject', '^test: ', 'discard'))
Header match on "subject"
>>> header_matches[0].delete()
>>> len(header_matches) # not automatically updated
1
>>> len(test_one.header_matches)
0
Configuration¶
Mailman Core exposes all its configuration through REST API. All these configuration options are read-only.
>>> cfg = client.configuration
>>> for key in sorted(cfg):
... print(cfg[key].name)
ARC
antispam
archiver.mail_archive
archiver.master
archiver.mhonarc
archiver.prototype
bounces
database
devmode
digests
dmarc
language.ar
language.ast
language.ca
language.cs
language.da
language.de
language.el
language.en
language.es
language.et
language.eu
language.fi
language.fr
language.gl
language.he
language.hr
language.hu
language.ia
language.it
language.ja
language.ko
language.lt
language.nl
language.no
language.pl
language.pt
language.pt_BR
language.ro
language.ru
language.sk
language.sl
language.sr
language.sv
language.tr
language.uk
language.vi
language.zh_CN
language.zh_TW
logging.archiver
logging.bounce
logging.config
logging.database
logging.debug
logging.error
logging.fromusenet
logging.http
logging.locks
logging.mischief
logging.plugins
logging.root
logging.runner
logging.smtp
logging.subscribe
logging.vette
mailman
mta
nntp
passwords
paths.dev
paths.fhs
paths.here
paths.local
plugin.master
runner.archive
runner.bad
runner.bounces
runner.command
runner.digest
runner.in
runner.lmtp
runner.nntp
runner.out
runner.pipeline
runner.rest
runner.retry
runner.shunt
runner.virgin
shell
styles
webservice
Each configuration object is a dictionary and you can iterate over them
>>> for key in sorted(cfg['mailman']):
... print('{} : {}'.format(key, cfg['mailman'][key]))
cache_life : 7d
default_language : en
email_commands_max_lines : 10
filtered_messages_are_preservable : no
html_to_plain_text_command : /usr/bin/lynx -dump $filename
layout : here
listname_chars : [-_.0-9a-z]
noreply_address : noreply
pending_request_life : 3d
post_hook :
pre_hook :
self_link : http://localhost:9001/3.1/system/configuration/mailman
sender_headers : from from_ reply-to sender
site_owner : changeme@example.com