summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--setup.py3
-rw-r--r--src/mailman/app/bounces.py9
-rw-r--r--src/mailman/app/docs/lifecycle.rst8
-rw-r--r--src/mailman/app/docs/subscriptions.rst24
-rw-r--r--src/mailman/app/events.py13
-rw-r--r--src/mailman/app/lifecycle.py22
-rw-r--r--src/mailman/app/membership.py7
-rw-r--r--src/mailman/app/registrar.py7
-rw-r--r--src/mailman/app/subscriptions.py70
-rw-r--r--src/mailman/app/templates.py5
-rw-r--r--src/mailman/app/tests/test_membership.py28
-rw-r--r--src/mailman/app/tests/test_moderation.py12
-rw-r--r--src/mailman/app/tests/test_subscriptions.py4
-rw-r--r--src/mailman/archiving/docs/common.rst6
-rw-r--r--src/mailman/archiving/mailarchive.py21
-rw-r--r--src/mailman/archiving/mhonarc.py7
-rw-r--r--src/mailman/archiving/prototype.py7
-rw-r--r--src/mailman/archiving/tests/test_prototype.py5
-rw-r--r--src/mailman/bin/mailman.py12
-rw-r--r--src/mailman/bin/set_members.py189
-rw-r--r--src/mailman/chains/accept.py11
-rw-r--r--src/mailman/chains/base.py26
-rw-r--r--src/mailman/chains/builtin.py7
-rw-r--r--src/mailman/chains/discard.py11
-rw-r--r--src/mailman/chains/docs/moderation.rst14
-rw-r--r--src/mailman/chains/headers.py6
-rw-r--r--src/mailman/chains/hold.py16
-rw-r--r--src/mailman/chains/moderation.py8
-rw-r--r--src/mailman/chains/owner.py10
-rw-r--r--src/mailman/chains/reject.py12
-rw-r--r--src/mailman/chains/tests/test_hold.py12
-rw-r--r--src/mailman/chains/tests/test_owner.py8
-rw-r--r--src/mailman/commands/cli_aliases.py14
-rw-r--r--src/mailman/commands/cli_control.py29
-rw-r--r--src/mailman/commands/cli_help.py7
-rw-r--r--src/mailman/commands/cli_import.py17
-rw-r--r--src/mailman/commands/cli_info.py32
-rw-r--r--src/mailman/commands/cli_inject.py13
-rw-r--r--src/mailman/commands/cli_lists.py39
-rw-r--r--src/mailman/commands/cli_members.py12
-rw-r--r--src/mailman/commands/cli_qfile.py15
-rw-r--r--src/mailman/commands/cli_status.py11
-rw-r--r--src/mailman/commands/cli_unshunt.py11
-rw-r--r--src/mailman/commands/cli_version.py9
-rw-r--r--src/mailman/commands/cli_withlist.py49
-rw-r--r--src/mailman/commands/docs/end.rst8
-rw-r--r--src/mailman/commands/docs/info.rst8
-rw-r--r--src/mailman/commands/eml_confirm.py13
-rw-r--r--src/mailman/commands/eml_echo.py4
-rw-r--r--src/mailman/commands/eml_end.py6
-rw-r--r--src/mailman/commands/eml_help.py9
-rw-r--r--src/mailman/commands/eml_membership.py14
-rw-r--r--src/mailman/config/config.py41
-rw-r--r--src/mailman/config/configure.zcml32
-rw-r--r--src/mailman/config/passlib.cfg10
-rw-r--r--src/mailman/config/schema.cfg20
-rw-r--r--src/mailman/config/tests/__init__.py0
-rw-r--r--src/mailman/config/tests/test_configuration.py53
-rw-r--r--src/mailman/core/constants.py7
-rw-r--r--src/mailman/core/i18n.py8
-rw-r--r--src/mailman/core/initialize.py22
-rw-r--r--src/mailman/core/pipelines.py5
-rw-r--r--src/mailman/core/runner.py17
-rw-r--r--src/mailman/core/switchboard.py37
-rw-r--r--src/mailman/core/system.py7
-rw-r--r--src/mailman/core/tests/test_pipelines.py10
-rw-r--r--src/mailman/core/tests/test_runner.py89
-rw-r--r--src/mailman/database/base.py69
-rw-r--r--src/mailman/database/docs/migration.rst92
-rw-r--r--src/mailman/database/factory.py101
-rw-r--r--src/mailman/database/model.py42
-rw-r--r--src/mailman/database/postgresql.py46
-rw-r--r--src/mailman/database/schema/mm_00000000000000_base.py22
-rw-r--r--src/mailman/database/schema/mm_20120407000000.py214
-rw-r--r--src/mailman/database/schema/postgres.sql3
-rw-r--r--src/mailman/database/schema/sqlite.sql3
-rw-r--r--src/mailman/database/schema/sqlite_20120407000000_01.sql280
-rw-r--r--src/mailman/database/sqlite.py32
-rw-r--r--src/mailman/database/tests/__init__.py0
-rw-r--r--src/mailman/database/tests/data/__init__.py0
-rw-r--r--src/mailman/database/tests/data/mailman_01.dbbin0 -> 48128 bytes
-rw-r--r--src/mailman/database/tests/data/migration_postgres_1.sql133
-rw-r--r--src/mailman/database/tests/data/migration_sqlite_1.sql133
-rw-r--r--src/mailman/database/tests/test_migrations.py369
-rw-r--r--src/mailman/database/transaction.py56
-rw-r--r--src/mailman/docs/ACKNOWLEDGMENTS.rst2
-rw-r--r--src/mailman/docs/DATABASE.rst8
-rw-r--r--src/mailman/docs/INTRODUCTION.rst4
-rw-r--r--src/mailman/docs/MTA.rst16
-rw-r--r--src/mailman/docs/NEWS.rst78
-rw-r--r--src/mailman/docs/START.rst13
-rw-r--r--src/mailman/docs/STYLEGUIDE.rst2
-rw-r--r--src/mailman/email/validate.py5
-rw-r--r--src/mailman/handlers/acknowledge.py6
-rw-r--r--src/mailman/handlers/after_delivery.py7
-rw-r--r--src/mailman/handlers/avoid_duplicates.py7
-rw-r--r--src/mailman/handlers/cleanse.py7
-rw-r--r--src/mailman/handlers/cleanse_dkim.py7
-rw-r--r--src/mailman/handlers/cook_headers.py7
-rw-r--r--src/mailman/handlers/decorate.py7
-rw-r--r--src/mailman/handlers/docs/archives.rst7
-rw-r--r--src/mailman/handlers/docs/rfc-2369.rst36
-rw-r--r--src/mailman/handlers/file_recipients.py7
-rw-r--r--src/mailman/handlers/member_recipients.py5
-rw-r--r--src/mailman/handlers/mime_delete.py7
-rw-r--r--src/mailman/handlers/owner_recipients.py5
-rw-r--r--src/mailman/handlers/replybot.py7
-rw-r--r--src/mailman/handlers/rfc_2369.py20
-rw-r--r--src/mailman/handlers/tagger.py7
-rw-r--r--src/mailman/handlers/to_archive.py11
-rw-r--r--src/mailman/handlers/to_digest.py7
-rw-r--r--src/mailman/handlers/to_outgoing.py7
-rw-r--r--src/mailman/handlers/to_usenet.py5
-rw-r--r--src/mailman/interfaces/archiver.py10
-rw-r--r--src/mailman/interfaces/chain.py37
-rw-r--r--src/mailman/interfaces/configuration.py41
-rw-r--r--src/mailman/interfaces/database.py34
-rw-r--r--src/mailman/interfaces/listmanager.py13
-rw-r--r--src/mailman/interfaces/mailinglist.py27
-rw-r--r--src/mailman/interfaces/member.py5
-rw-r--r--src/mailman/interfaces/nntp.py8
-rw-r--r--src/mailman/interfaces/runner.py13
-rw-r--r--src/mailman/interfaces/subscriptions.py26
-rw-r--r--src/mailman/languages/language.py8
-rw-r--r--src/mailman/languages/manager.py25
-rw-r--r--src/mailman/model/address.py7
-rw-r--r--src/mailman/model/autorespond.py29
-rw-r--r--src/mailman/model/bans.py45
-rw-r--r--src/mailman/model/bounce.py28
-rw-r--r--src/mailman/model/digests.py7
-rw-r--r--src/mailman/model/docs/listmanager.rst66
-rw-r--r--src/mailman/model/docs/mailinglist.rst11
-rw-r--r--src/mailman/model/docs/membership.rst12
-rw-r--r--src/mailman/model/docs/pending.rst9
-rw-r--r--src/mailman/model/docs/registration.rst9
-rw-r--r--src/mailman/model/docs/requests.rst2
-rw-r--r--src/mailman/model/docs/usermanager.rst11
-rw-r--r--src/mailman/model/docs/users.rst11
-rw-r--r--src/mailman/model/domain.py48
-rw-r--r--src/mailman/model/language.py7
-rw-r--r--src/mailman/model/listmanager.py70
-rw-r--r--src/mailman/model/mailinglist.py42
-rw-r--r--src/mailman/model/member.py38
-rw-r--r--src/mailman/model/message.py15
-rw-r--r--src/mailman/model/messagestore.py38
-rw-r--r--src/mailman/model/mime.py8
-rw-r--r--src/mailman/model/pending.py33
-rw-r--r--src/mailman/model/preferences.py7
-rw-r--r--src/mailman/model/requests.py74
-rw-r--r--src/mailman/model/roster.py66
-rw-r--r--src/mailman/model/tests/test_bounce.py25
-rw-r--r--src/mailman/model/uid.py18
-rw-r--r--src/mailman/model/user.py26
-rw-r--r--src/mailman/model/usermanager.py56
-rw-r--r--src/mailman/model/version.py6
-rw-r--r--src/mailman/mta/aliases.py7
-rw-r--r--src/mailman/mta/base.py7
-rw-r--r--src/mailman/mta/null.py8
-rw-r--r--src/mailman/mta/postfix.py22
-rw-r--r--src/mailman/rest/addresses.py2
-rw-r--r--src/mailman/rest/configuration.py7
-rw-r--r--src/mailman/rest/docs/addresses.rst10
-rw-r--r--src/mailman/rest/docs/configuration.rst25
-rw-r--r--src/mailman/rest/docs/membership.rst108
-rw-r--r--src/mailman/rest/docs/users.rst14
-rw-r--r--src/mailman/rest/lists.py4
-rw-r--r--src/mailman/rest/members.py10
-rw-r--r--src/mailman/rest/tests/test_addresses.py9
-rw-r--r--src/mailman/rest/tests/test_domains.py13
-rw-r--r--src/mailman/rest/tests/test_lists.py22
-rw-r--r--src/mailman/rest/tests/test_membership.py68
-rw-r--r--src/mailman/rest/tests/test_moderation.py5
-rw-r--r--src/mailman/rest/tests/test_users.py7
-rw-r--r--src/mailman/rest/users.py5
-rw-r--r--src/mailman/rest/wsgiapp.py13
-rw-r--r--src/mailman/rules/administrivia.py6
-rw-r--r--src/mailman/rules/any.py6
-rw-r--r--src/mailman/rules/approved.py16
-rw-r--r--src/mailman/rules/docs/approved.rst5
-rw-r--r--src/mailman/rules/docs/header-matching.rst10
-rw-r--r--src/mailman/rules/docs/news-moderation.rst6
-rw-r--r--src/mailman/rules/emergency.py6
-rw-r--r--src/mailman/rules/implicit_dest.py6
-rw-r--r--src/mailman/rules/loop.py6
-rw-r--r--src/mailman/rules/max_recipients.py6
-rw-r--r--src/mailman/rules/max_size.py6
-rw-r--r--src/mailman/rules/moderation.py8
-rw-r--r--src/mailman/rules/news_moderation.py10
-rw-r--r--src/mailman/rules/no_subject.py6
-rw-r--r--src/mailman/rules/suspicious.py6
-rw-r--r--src/mailman/rules/tests/test_approved.py92
-rw-r--r--src/mailman/rules/truth.py6
-rw-r--r--src/mailman/runners/command.py7
-rw-r--r--src/mailman/runners/digest.py6
-rw-r--r--src/mailman/runners/docs/incoming.rst10
-rw-r--r--src/mailman/runners/incoming.py16
-rw-r--r--src/mailman/runners/lmtp.py31
-rw-r--r--src/mailman/runners/nntp.py10
-rw-r--r--src/mailman/runners/tests/test_archiver.py6
-rw-r--r--src/mailman/runners/tests/test_bounce.py11
-rw-r--r--src/mailman/runners/tests/test_confirm.py11
-rw-r--r--src/mailman/runners/tests/test_lmtp.py6
-rw-r--r--src/mailman/runners/tests/test_nntp.py18
-rw-r--r--src/mailman/runners/tests/test_owner.py23
-rw-r--r--src/mailman/runners/tests/test_pipeline.py9
-rw-r--r--src/mailman/styles/default.py22
-rw-r--r--src/mailman/styles/docs/styles.rst6
-rw-r--r--src/mailman/styles/manager.py16
-rw-r--r--src/mailman/styles/tests/test_styles.py4
-rw-r--r--src/mailman/testing/helpers.py76
-rw-r--r--src/mailman/testing/layers.py17
-rw-r--r--src/mailman/testing/mta.py7
-rw-r--r--src/mailman/testing/passlib.cfg4
-rw-r--r--src/mailman/testing/testing.cfg9
-rw-r--r--src/mailman/tests/test_documentation.py27
-rw-r--r--src/mailman/utilities/importer.py9
-rw-r--r--src/mailman/utilities/passwords.py62
-rw-r--r--src/mailman/utilities/tests/test_import.py9
-rw-r--r--src/mailman/utilities/tests/test_passwords.py60
219 files changed, 3632 insertions, 1684 deletions
diff --git a/setup.py b/setup.py
index 89f67a899..236faa3f5 100644
--- a/setup.py
+++ b/setup.py
@@ -98,16 +98,17 @@ case second `m'. Any other spelling is incorrect.""",
'flufl.enum',
'flufl.i18n',
'flufl.lock',
- 'flufl.password',
'httplib2',
'lazr.config',
'lazr.smtptest',
'mock',
+ 'passlib',
'restish',
'storm',
'zc.buildout',
'zope.component',
'zope.configuration',
+ 'zope.event',
'zope.interface',
'zope.testing<4',
],
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py
index 4107a8c6e..41a8f3560 100644
--- a/src/mailman/app/bounces.py
+++ b/src/mailman/app/bounces.py
@@ -17,7 +17,7 @@
"""Application level bounce handling."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -38,7 +38,7 @@ from email.mime.text import MIMEText
from email.utils import parseaddr
from string import Template
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -176,9 +176,9 @@ class ProbeVERP(_BaseVERPParser):
+@implementer(IPendable)
class _ProbePendable(dict):
"""The pendable dictionary for probe messages."""
- implements(IPendable)
def send_probe(member, msg):
@@ -192,7 +192,8 @@ def send_probe(member, msg):
:return: The token representing this probe in the pendings database.
:rtype: string
"""
- mlist = getUtility(IListManager).get(member.mailing_list)
+ mlist = getUtility(IListManager).get_by_list_id(
+ member.mailing_list.list_id)
text = make('probe.txt', mlist, member.preferred_language.code,
listname=mlist.fqdn_listname,
address= member.address.email,
diff --git a/src/mailman/app/docs/lifecycle.rst b/src/mailman/app/docs/lifecycle.rst
index 08a25ccff..f6bb7ddae 100644
--- a/src/mailman/app/docs/lifecycle.rst
+++ b/src/mailman/app/docs/lifecycle.rst
@@ -44,10 +44,10 @@ Creating a list applies its styles
Start by registering a test style.
::
- >>> from zope.interface import implements
+ >>> from zope.interface import implementer
>>> from mailman.interfaces.styles import IStyle
- >>> class TestStyle(object):
- ... implements(IStyle)
+ >>> @implementer(IStyle)
+ ... class TestStyle(object):
... name = 'test'
... priority = 10
... def apply(self, mailing_list):
@@ -140,7 +140,7 @@ artifacts.
::
>>> from mailman.app.lifecycle import remove_list
- >>> remove_list(mlist_2.fqdn_listname, mlist_2)
+ >>> remove_list(mlist_2)
>>> from mailman.interfaces.listmanager import IListManager
>>> from zope.component import getUtility
diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst
index f897d219e..dd8298cb3 100644
--- a/src/mailman/app/docs/subscriptions.rst
+++ b/src/mailman/app/docs/subscriptions.rst
@@ -30,13 +30,13 @@ role. At a minimum, a mailing list and an address for the new user is
required.
>>> mlist = create_list('test@example.com')
- >>> anne = service.join('test@example.com', 'anne@example.com')
+ >>> anne = service.join('test.example.com', 'anne@example.com')
>>> anne
<Member: anne <anne@example.com> on test@example.com as MemberRole.member>
The real name of the new member can be given.
- >>> bart = service.join('test@example.com', 'bart@example.com',
+ >>> bart = service.join('test.example.com', 'bart@example.com',
... 'Bart Person')
>>> bart
<Member: Bart Person <bart@example.com>
@@ -45,7 +45,7 @@ The real name of the new member can be given.
Other roles can also be subscribed.
>>> from mailman.interfaces.member import MemberRole
- >>> anne_owner = service.join('test@example.com', 'anne@example.com',
+ >>> anne_owner = service.join('test.example.com', 'anne@example.com',
... role=MemberRole.owner)
>>> anne_owner
<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
@@ -67,7 +67,7 @@ New members can also be added by providing an existing user id instead of an
email address. However, the user must have a preferred email address.
::
- >>> service.join('test@example.com', bart.user.user_id,
+ >>> service.join('test.example.com', bart.user.user_id,
... role=MemberRole.owner)
Traceback (most recent call last):
...
@@ -78,7 +78,7 @@ email address. However, the user must have a preferred email address.
>>> address = list(bart.user.addresses)[0]
>>> address.verified_on = now()
>>> bart.user.preferred_address = address
- >>> service.join('test@example.com', bart.user.user_id,
+ >>> service.join('test.example.com', bart.user.user_id,
... role=MemberRole.owner)
<Member: Bart Person <bart@example.com>
on test@example.com as MemberRole.owner>
@@ -89,7 +89,7 @@ Removing members
Regular members can also be removed.
- >>> cris = service.join('test@example.com', 'cris@example.com')
+ >>> cris = service.join('test.example.com', 'cris@example.com')
>>> service.get_members()
[<Member: anne <anne@example.com> on test@example.com
as MemberRole.owner>,
@@ -103,7 +103,7 @@ Regular members can also be removed.
as MemberRole.member>]
>>> sum(1 for member in service)
5
- >>> service.leave('test@example.com', 'cris@example.com')
+ >>> service.leave('test.example.com', 'cris@example.com')
>>> service.get_members()
[<Member: anne <anne@example.com> on test@example.com
as MemberRole.owner>,
@@ -173,7 +173,7 @@ Memberships can also be searched for by user id.
You can find all the memberships for a specific mailing list.
- >>> service.find_members(fqdn_listname='test@example.com')
+ >>> service.find_members(list_id='test.example.com')
[<Member: anne <anne@example.com> on test@example.com
as MemberRole.member>,
<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
@@ -184,9 +184,11 @@ You can find all the memberships for a specific mailing list.
<Member: Bart Person <bart@example.com> on test@example.com
as MemberRole.owner>]
-You can find all the memberships for an address on a specific mailing list.
+You can find all the memberships for an address on a specific mailing list,
+but you have to give it the list id, not the fqdn listname since the former is
+stable but the latter could change if the list is moved.
- >>> service.find_members('anne@example.com', 'test@example.com')
+ >>> service.find_members('anne@example.com', 'test.example.com')
[<Member: anne <anne@example.com> on test@example.com
as MemberRole.member>,
<Member: anne <anne@example.com> on test@example.com
@@ -203,7 +205,7 @@ You can find all the memberships for an address with a specific role.
You can also find a specific membership by all three criteria.
- >>> service.find_members('anne@example.com', 'test@example.com',
+ >>> service.find_members('anne@example.com', 'test.example.com',
... MemberRole.owner)
[<Member: anne <anne@example.com> on test@example.com
as MemberRole.owner>]
diff --git a/src/mailman/app/events.py b/src/mailman/app/events.py
index 79376cca1..28d78e001 100644
--- a/src/mailman/app/events.py
+++ b/src/mailman/app/events.py
@@ -28,13 +28,22 @@ __all__ = [
from zope import event
from mailman.app import domain, moderator, subscriptions
+from mailman.core import i18n, switchboard
+from mailman.languages import manager as language_manager
+from mailman.styles import manager as style_manager
+from mailman.utilities import passwords
def initialize():
"""Initialize global event subscribers."""
event.subscribers.extend([
- moderator.handle_ListDeletingEvent,
- subscriptions.handle_ListDeletedEvent,
domain.handle_DomainDeletingEvent,
+ moderator.handle_ListDeletingEvent,
+ passwords.handle_ConfigurationUpdatedEvent,
+ subscriptions.handle_ListDeletingEvent,
+ switchboard.handle_ConfigurationUpdatedEvent,
+ i18n.handle_ConfigurationUpdatedEvent,
+ style_manager.handle_ConfigurationUpdatedEvent,
+ language_manager.handle_ConfigurationUpdatedEvent,
])
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py
index 5082034bc..326498478 100644
--- a/src/mailman/app/lifecycle.py
+++ b/src/mailman/app/lifecycle.py
@@ -89,23 +89,19 @@ def create_list(fqdn_listname, owners=None):
-def remove_list(fqdn_listname, mailing_list=None):
+def remove_list(mlist):
"""Remove the list and all associated artifacts and subscriptions."""
+ fqdn_listname = mlist.fqdn_listname
removeables = []
- # mailing_list will be None when only residual archives are being removed.
- if mailing_list is not None:
- # Remove all subscriptions, regardless of role.
- for member in mailing_list.subscribers.members:
- member.unsubscribe()
- # Delete the mailing list from the database.
- getUtility(IListManager).delete(mailing_list)
- # Do the MTA-specific list deletion tasks
- call_name(config.mta.incoming).create(mailing_list)
- # Remove the list directory.
- removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
+ # Delete the mailing list from the database.
+ getUtility(IListManager).delete(mlist)
+ # Do the MTA-specific list deletion tasks
+ call_name(config.mta.incoming).delete(mlist)
+ # Remove the list directory.
+ removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
# Remove any stale locks associated with the list.
for filename in os.listdir(config.LOCK_DIR):
- fn_listname = filename.split('.')[0]
+ fn_listname, dot, rest = filename.partition('.')
if fn_listname == fqdn_listname:
removeables.append(os.path.join(config.LOCK_DIR, filename))
# Now that we know what files and directories to delete, delete them.
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index e31a1695c..c73735b35 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -27,7 +27,6 @@ __all__ = [
from email.utils import formataddr
-from flufl.password import lookup, make_secret
from zope.component import getUtility
from mailman.app.notifications import send_goodbye_message
@@ -96,10 +95,8 @@ def add_member(mlist, email, display_name, password, delivery_mode, language,
user.display_name = (
display_name if display_name else address.display_name)
user.link(address)
- # Encrypt the password using the currently selected scheme. The
- # scheme is recorded in the hashed password string.
- scheme = lookup(config.passwords.password_scheme.upper())
- user.password = make_secret(password, scheme)
+ # Encrypt the password using the currently selected hash scheme.
+ user.password = config.password_context.encrypt(password)
user.preferences.preferred_language = language
member = mlist.subscribe(address, role)
member.preferences.delivery_mode = delivery_mode
diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py
index 030a504f7..63e3c5144 100644
--- a/src/mailman/app/registrar.py
+++ b/src/mailman/app/registrar.py
@@ -28,7 +28,7 @@ __all__ = [
import logging
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.notifications import send_welcome_message
from mailman.core.i18n import _
@@ -47,17 +47,16 @@ log = logging.getLogger('mailman.error')
+@implementer(IPendable)
class PendableRegistration(dict):
- implements(IPendable)
PEND_KEY = 'registration'
+@implementer(IRegistrar)
class Registrar:
"""Handle registrations and confirmations for subscriptions."""
- implements(IRegistrar)
-
def register(self, mlist, email, display_name=None, delivery_mode=None):
"""See `IUserRegistrar`."""
if delivery_mode is None:
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 60f8cdebe..3937d5b5c 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -17,28 +17,29 @@
"""Module stuff."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'SubscriptionService',
- 'handle_ListDeletedEvent',
+ 'handle_ListDeletingEvent',
]
-from flufl.password import generate
from operator import attrgetter
+from passlib.utils import generate_password as generate
from storm.expr import And, Or
from uuid import UUID
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.membership import add_member, delete_member
from mailman.config import config
from mailman.core.constants import system_preferences
+from mailman.database.transaction import dbconnection
from mailman.interfaces.address import IEmailValidator
from mailman.interfaces.listmanager import (
- IListManager, ListDeletedEvent, NoSuchListError)
+ IListManager, ListDeletingEvent, NoSuchListError)
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError)
@@ -48,40 +49,35 @@ from mailman.model.member import Member
def _membership_sort_key(member):
- """Sort function for get_members().
+ """Sort function for find_members().
- The members are sorted first by fully-qualified mailing list name,
- then by subscribed email address, then by role.
+ The members are sorted first by unique list id, then by subscribed email
+ address, then by role.
"""
- # member.mailing_list is already the fqdn_listname, not the IMailingList
- # object.
- return (member.mailing_list,
- member.address.email,
- int(member.role))
+ return (member.list_id, member.address.email, int(member.role))
+@implementer(ISubscriptionService)
class SubscriptionService:
"""Subscription services for the REST API."""
- implements(ISubscriptionService)
-
__name__ = 'members'
def get_members(self):
"""See `ISubscriptionService`."""
- # {fqdn_listname -> {role -> [members]}}
+ # {list_id -> {role -> [members]}}
by_list = {}
user_manager = getUtility(IUserManager)
for member in user_manager.members:
- by_role = by_list.setdefault(member.mailing_list, {})
+ by_role = by_list.setdefault(member.list_id, {})
members = by_role.setdefault(member.role.name, [])
members.append(member)
# Flatten into single list sorted as per the interface.
all_members = []
address_of_member = attrgetter('address.email')
- for fqdn_listname in sorted(by_list):
- by_role = by_list[fqdn_listname]
+ for list_id in sorted(by_list):
+ by_role = by_list[list_id]
all_members.extend(
sorted(by_role.get('owner', []), key=address_of_member))
all_members.extend(
@@ -90,9 +86,10 @@ class SubscriptionService:
sorted(by_role.get('member', []), key=address_of_member))
return all_members
- def get_member(self, member_id):
+ @dbconnection
+ def get_member(self, store, member_id):
"""See `ISubscriptionService`."""
- members = config.db.store.find(
+ members = store.find(
Member,
Member._member_id == member_id)
if members.count() == 0:
@@ -101,13 +98,14 @@ class SubscriptionService:
assert members.count() == 1, 'Too many matching members'
return members[0]
- def find_members(self, subscriber=None, fqdn_listname=None, role=None):
+ @dbconnection
+ def find_members(self, store, subscriber=None, list_id=None, role=None):
"""See `ISubscriptionService`."""
# If `subscriber` is a user id, then we'll search for all addresses
# which are controlled by the user, otherwise we'll just search for
# the given address.
user_manager = getUtility(IUserManager)
- if subscriber is None and fqdn_listname is None and role is None:
+ if subscriber is None and list_id is None and role is None:
return []
# Querying for the subscriber is the most complicated part, because
# the parameter can either be an email address or a user id.
@@ -133,25 +131,25 @@ class SubscriptionService:
Member.address_id.is_in(address_ids)))
# Calculate the rest of the query expression, which will get And'd
# with the Or clause above (if there is one).
- if fqdn_listname is not None:
- query.append(Member.mailing_list == fqdn_listname)
+ if list_id is not None:
+ query.append(Member.list_id == list_id)
if role is not None:
query.append(Member.role == role)
- results = config.db.store.find(Member, And(*query))
+ results = store.find(Member, And(*query))
return sorted(results, key=_membership_sort_key)
def __iter__(self):
for member in self.get_members():
yield member
- def join(self, fqdn_listname, subscriber,
+ def join(self, list_id, subscriber,
display_name=None,
delivery_mode=DeliveryMode.regular,
role=MemberRole.member):
"""See `ISubscriptionService`."""
- mlist = getUtility(IListManager).get(fqdn_listname)
+ mlist = getUtility(IListManager).get_by_list_id(list_id)
if mlist is None:
- raise NoSuchListError(fqdn_listname)
+ raise NoSuchListError(list_id)
# Is the subscriber an email address or user id?
if isinstance(subscriber, basestring):
# It's an email address, so we'll want a real name. Make sure
@@ -178,23 +176,23 @@ class SubscriptionService:
raise MissingUserError(subscriber)
return mlist.subscribe(user, role)
- def leave(self, fqdn_listname, email):
+ def leave(self, list_id, email):
"""See `ISubscriptionService`."""
- mlist = getUtility(IListManager).get(fqdn_listname)
+ mlist = getUtility(IListManager).get_by_list_id(list_id)
if mlist is None:
- raise NoSuchListError(fqdn_listname)
+ raise NoSuchListError(list_id)
# XXX for now, no notification or user acknowledgment.
delete_member(mlist, email, False, False)
-def handle_ListDeletedEvent(event):
- """Delete a mailing list's members when the list is deleted."""
+def handle_ListDeletingEvent(event):
+ """Delete a mailing list's members when the list is being deleted."""
- if not isinstance(event, ListDeletedEvent):
+ if not isinstance(event, ListDeletingEvent):
return
# Find all the members still associated with the mailing list.
members = getUtility(ISubscriptionService).find_members(
- fqdn_listname=event.fqdn_listname)
+ list_id=event.mailing_list.list_id)
for member in members:
member.unsubscribe()
diff --git a/src/mailman/app/templates.py b/src/mailman/app/templates.py
index a91231cfc..f29781e58 100644
--- a/src/mailman/app/templates.py
+++ b/src/mailman/app/templates.py
@@ -31,7 +31,7 @@ from contextlib import closing
from urllib import addinfourl
from urlparse import urlparse
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.utilities.i18n import TemplateNotFoundError, find
from mailman.interfaces.languages import ILanguageManager
@@ -92,11 +92,10 @@ class MailmanHandler(urllib2.BaseHandler):
+@implementer(ITemplateLoader)
class TemplateLoader:
"""Loader of templates, with caching and support for mailman:// URIs."""
- implements(ITemplateLoader)
-
def __init__(self):
opener = urllib2.build_opener(MailmanHandler())
urllib2.install_opener(opener)
diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py
index 74e15d0e2..00c279910 100644
--- a/src/mailman/app/tests/test_membership.py
+++ b/src/mailman/app/tests/test_membership.py
@@ -30,13 +30,11 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
-from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.interfaces.bans import IBanManager
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, MemberRole, MembershipIsBannedError)
from mailman.interfaces.usermanager import IUserManager
-from mailman.testing.helpers import reset_the_world
from mailman.testing.layers import ConfigLayer
@@ -47,9 +45,6 @@ class AddMemberTest(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- def tearDown(self):
- reset_the_world()
-
def test_add_member_new_user(self):
# Test subscribing a user to a mailing list when the email address has
# not yet been associated with a user.
@@ -57,7 +52,7 @@ class AddMemberTest(unittest.TestCase):
'Anne Person', '123', DeliveryMode.regular,
system_preferences.preferred_language)
self.assertEqual(member.address.email, 'aperson@example.com')
- self.assertEqual(member.mailing_list, 'test@example.com')
+ self.assertEqual(member.list_id, 'test.example.com')
self.assertEqual(member.role, MemberRole.member)
def test_add_member_existing_user(self):
@@ -69,7 +64,7 @@ class AddMemberTest(unittest.TestCase):
'Anne Person', '123', DeliveryMode.regular,
system_preferences.preferred_language)
self.assertEqual(member.address.email, 'aperson@example.com')
- self.assertEqual(member.mailing_list, 'test@example.com')
+ self.assertEqual(member.list_id, 'test.example.com')
def test_add_member_banned(self):
# Test that members who are banned by specific address cannot
@@ -132,9 +127,9 @@ class AddMemberTest(unittest.TestCase):
system_preferences.preferred_language,
MemberRole.moderator)
self.assertEqual(member.address.email, 'aperson@example.com')
- self.assertEqual(member.mailing_list, 'test@example.com')
+ self.assertEqual(member.list_id, 'test.example.com')
self.assertEqual(member.role, MemberRole.moderator)
-
+
def test_add_member_twice(self):
# Adding a member with the same role twice causes an
# AlreadySubscribedError to be raised.
@@ -164,7 +159,7 @@ class AddMemberTest(unittest.TestCase):
'Anne Person', '123', DeliveryMode.regular,
system_preferences.preferred_language,
MemberRole.owner)
- self.assertEqual(member_1.mailing_list, member_2.mailing_list)
+ self.assertEqual(member_1.list_id, member_2.list_id)
self.assertEqual(member_1.address, member_2.address)
self.assertEqual(member_1.user, member_2.user)
self.assertNotEqual(member_1.member_id, member_2.member_id)
@@ -178,21 +173,10 @@ class AddMemberPasswordTest(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- # The default ssha scheme introduces a random salt, which is
- # inappropriate for unit tests.
- config.push('password scheme', """
- [passwords]
- password_scheme: sha
- """)
-
- def tearDown(self):
- config.pop('password scheme')
- reset_the_world()
def test_add_member_password(self):
# Test that the password stored with the new user is encrypted.
member = add_member(self._mlist, 'anne@example.com',
'Anne Person', 'abc', DeliveryMode.regular,
system_preferences.preferred_language)
- self.assertEqual(
- member.user.password, '{SHA}qZk-NkcGgWq6PiVxeFDCbJzQ2J0=')
+ self.assertEqual(member.user.password, '{plaintext}abc')
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index bc324faea..ef6adf5ed 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -37,6 +37,7 @@ from mailman.runners.pipeline import PipelineRunner
from mailman.testing.helpers import (
make_testable_runner, specialized_message_from_string)
from mailman.testing.layers import SMTPLayer
+from mailman.utilities.datetime import now
@@ -109,3 +110,14 @@ Message-ID: <alpha>
handle_message(self._mlist, request_id, Action.hold)
key, data = requests_db.get_request(request_id)
self.assertEqual(key, '<alpha>')
+
+ def test_lp_1031391(self):
+ # LP: #1031391 msgdata['received_time'] gets added by the LMTP server.
+ # The value is a datetime. If this message gets held, it will break
+ # pending requests since they require string keys and values.
+ received_time = now()
+ msgdata = dict(received_time=received_time)
+ request_id = hold_message(self._mlist, self._msg, msgdata)
+ requests_db = IListRequests(self._mlist)
+ key, data = requests_db.get_request(request_id)
+ self.assertEqual(data['received_time'], received_time)
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index a63c9ac04..1c37d4cb9 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -52,7 +52,7 @@ class TestJoin(unittest.TestCase):
def test_join_user_with_bogus_id(self):
# When `subscriber` is a missing user id, an exception is raised.
try:
- self._service.join('test@example.com', uuid.UUID(int=99))
+ self._service.join('test.example.com', uuid.UUID(int=99))
except MissingUserError as exc:
self.assertEqual(exc.user_id, uuid.UUID(int=99))
else:
@@ -62,7 +62,7 @@ class TestJoin(unittest.TestCase):
# When `subscriber` is a string that is not an email address, an
# exception is raised.
try:
- self._service.join('test@example.com', 'bogus')
+ self._service.join('test.example.com', 'bogus')
except InvalidEmailAddressError as exc:
self.assertEqual(exc.email, 'bogus')
else:
diff --git a/src/mailman/archiving/docs/common.rst b/src/mailman/archiving/docs/common.rst
index 7437f4790..86488b26e 100644
--- a/src/mailman/archiving/docs/common.rst
+++ b/src/mailman/archiving/docs/common.rst
@@ -72,6 +72,8 @@ To archive the message, the archiver actually mails the message to a special
address at The Mail Archive. The message gets no header or footer decoration.
::
+ >>> from mailman.interfaces.archiver import ArchivePolicy
+ >>> mlist.archive_policy = ArchivePolicy.public
>>> archiver.archive_message(mlist, msg)
>>> from mailman.runners.outgoing import OutgoingRunner
@@ -101,7 +103,7 @@ address at The Mail Archive. The message gets no header or footer decoration.
However, if the mailing list is not public, the message will never be archived
at this service.
- >>> mlist.archive_private = True
+ >>> mlist.archive_policy = ArchivePolicy.private
>>> print archiver.list_url(mlist)
None
>>> print archiver.permalink(mlist, msg)
@@ -114,7 +116,7 @@ Additionally, this archiver can handle malformed ``Message-IDs``.
::
>>> from mailman.utilities.email import add_message_hash
- >>> mlist.archive_private = False
+ >>> mlist.archive_policy = ArchivePolicy.public
>>> del msg['message-id']
>>> del msg['x-message-id-hash']
>>> msg['Message-ID'] = '12345>'
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index c72cde11c..e61683a09 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -17,7 +17,7 @@
"""The Mail-Archive.com archiver."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,35 +27,34 @@ __all__ = [
from urllib import quote
from urlparse import urljoin
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
-from mailman.interfaces.archiver import IArchiver
+from mailman.interfaces.archiver import ArchivePolicy, IArchiver
+@implementer(IArchiver)
class MailArchive:
"""Public archiver at the Mail-Archive.com.
Messages get archived at http://go.mail-archive.com.
"""
- implements(IArchiver)
-
name = 'mail-archive'
@staticmethod
def list_url(mlist):
"""See `IArchiver`."""
- if mlist.archive_private:
- return None
- return urljoin(config.archiver.mail_archive.base_url,
- quote(mlist.posting_address))
+ if mlist.archive_policy is ArchivePolicy.public:
+ return urljoin(config.archiver.mail_archive.base_url,
+ quote(mlist.posting_address))
+ return None
@staticmethod
def permalink(mlist, msg):
"""See `IArchiver`."""
- if mlist.archive_private:
+ if mlist.archive_policy is not ArchivePolicy.public:
return None
# It is the LMTP server's responsibility to ensure that the message
# has a X-Message-ID-Hash header. If it doesn't then there's no
@@ -68,7 +67,7 @@ class MailArchive:
@staticmethod
def archive_message(mlist, msg):
"""See `IArchiver`."""
- if not mlist.archive_private:
+ if mlist.archive_policy is ArchivePolicy.public:
config.switchboards['out'].enqueue(
msg,
listname=mlist.fqdn_listname,
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index 0beeed73e..7f0af6cd6 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -17,7 +17,7 @@
"""MHonArc archiver."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,7 +29,7 @@ import logging
import subprocess
from urlparse import urljoin
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
@@ -40,11 +40,10 @@ log = logging.getLogger('mailman.archiver')
+@implementer(IArchiver)
class MHonArc:
"""Local MHonArc archiver."""
- implements(IArchiver)
-
name = 'mhonarc'
@staticmethod
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index 453c6c770..3ce51ddb5 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -17,7 +17,7 @@
"""Prototypical permalinking archiver."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -34,7 +34,7 @@ from mailbox import Maildir
from urlparse import urljoin
from flufl.lock import Lock, TimeOutError
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
@@ -43,6 +43,7 @@ log = logging.getLogger('mailman.error')
+@implementer(IArchiver)
class Prototype:
"""A prototype of a third party archiver.
@@ -50,8 +51,6 @@ class Prototype:
servers and archivers: <http://wiki.list.org/display/DEV/Stable+URLs>.
"""
- implements(IArchiver)
-
name = 'prototype'
@staticmethod
diff --git a/src/mailman/archiving/tests/test_prototype.py b/src/mailman/archiving/tests/test_prototype.py
index 29f6ba1cb..bc1cee8b9 100644
--- a/src/mailman/archiving/tests/test_prototype.py
+++ b/src/mailman/archiving/tests/test_prototype.py
@@ -37,6 +37,7 @@ from flufl.lock import Lock
from mailman.app.lifecycle import create_list
from mailman.archiving.prototype import Prototype
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.testing.helpers import LogFileMark
from mailman.testing.helpers import (
specialized_message_from_string as mfs)
@@ -61,8 +62,8 @@ X-Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW
Tests are better than no tests
but the water deserves to be swum.
""")
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
# Set up a temporary directory for the prototype archiver so that it's
# easier to clean up.
self._tempdir = tempfile.mkdtemp()
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py
index 94de65255..6b15c9838 100644
--- a/src/mailman/bin/mailman.py
+++ b/src/mailman/bin/mailman.py
@@ -90,13 +90,9 @@ def main():
# No arguments or subcommands were given.
parser.print_help()
parser.exit()
- # Before actually performing the subcommand, we need to initialize the
- # Mailman system, and in particular, we must read the configuration file.
- config_file = os.getenv('MAILMAN_CONFIG_FILE')
- if config_file is None:
- if args.config is not None:
- config_file = os.path.abspath(os.path.expanduser(args.config))
-
- initialize(config_file)
+ # Initialize the system. Honor the -C flag if given.
+ config_path = (None if args.config is None
+ else os.path.abspath(os.path.expanduser(args.config)))
+ initialize(config_path)
# Perform the subcommand option.
args.func(args)
diff --git a/src/mailman/bin/set_members.py b/src/mailman/bin/set_members.py
deleted file mode 100644
index 6ec66af06..000000000
--- a/src/mailman/bin/set_members.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# Copyright (C) 2007-2012 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman 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 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman 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
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-import csv
-import optparse
-
-from zope.component import getUtility
-
-from mailman import Utils
-from mailman import passwords
-from mailman.app.membership import add_member
-from mailman.app.notifications import (
- send_admin_subscription_notice, send_welcome_message)
-from mailman.configuration import config
-from mailman.core.i18n import _
-from mailman.initialize import initialize
-from mailman.interfaces.members import DeliveryMode
-from mailman.interfaces.usermanager import IUserManager
-from mailman.version import MAILMAN_VERSION
-
-
-DELIVERY_MODES = {
- 'regular': DeliveryMode.regular,
- 'plain': DeliveryMode.plaintext_digests,
- 'mime': DeliveryMode.mime_digests,
- }
-
-
-
-def parseargs():
- parser = optparse.OptionParser(version=MAILMAN_VERSION,
- usage=_("""\
-%prog [options] csv-file
-
-Set the membership of a mailing list to that described in a CSV file. Each
-row of the CSV file has the following format. Only the address column is
-required.
-
- - email address
- - full name (default: the empty string)
- - delivery mode (default: regular delivery) [1]
-
-[1] The delivery mode is a case insensitive string of the following values:
-
- regular - regular, i.e. immediate delivery
- mime - MIME digest delivery
- plain - plain text (RFC 1153) digest delivery
-
-Any address not included in the CSV file is removed from the list membership.
-"""))
- parser.add_option('-l', '--listname',
- type='string', help=_("""\
-Mailng list to set the membership for."""))
- parser.add_option('-w', '--welcome-msg',
- type='string', metavar='<y|n>', help=_("""\
-Set whether or not to send the list members a welcome message, overriding
-whatever the list's 'send_welcome_msg' setting is."""))
- parser.add_option('-a', '--admin-notify',
- type='string', metavar='<y|n>', help=_("""\
-Set whether or not to send the list administrators a notification on the
-success/failure of these subscriptions, overriding whatever the list's
-'admin_notify_mchanges' setting is."""))
- parser.add_option('-v', '--verbose', action='store_true',
- help=_('Increase verbosity'))
- parser.add_option('-C', '--config',
- help=_('Alternative configuration file to use'))
- opts, args = parser.parse_args()
- if opts.welcome_msg is not None:
- ch = opts.welcome_msg[0].lower()
- if ch == 'y':
- opts.welcome_msg = True
- elif ch == 'n':
- opts.welcome_msg = False
- else:
- parser.error(_('Illegal value for -w: $opts.welcome_msg'))
- if opts.admin_notify is not None:
- ch = opts.admin_notify[0].lower()
- if ch == 'y':
- opts.admin_notify = True
- elif ch == 'n':
- opts.admin_notify = False
- else:
- parser.error(_('Illegal value for -a: $opts.admin_notify'))
- return parser, opts, args
-
-
-
-def parse_file(filename):
- members = {}
- with open(filename) as fp:
- for row in csv.reader(fp):
- if len(row) == 0:
- continue
- elif len(row) == 1:
- address = row[0]
- real_name = None
- delivery_mode = DeliveryMode.regular
- elif len(row) == 2:
- address, real_name = row
- delivery_mode = DeliveryMode.regular
- else:
- # Ignore extra columns
- address, real_name = row[0:2]
- delivery_mode = DELIVERY_MODES.get(row[2].lower())
- if delivery_mode is None:
- delivery_mode = DeliveryMode.regular
- members[address] = real_name, delivery_mode
- return members
-
-
-
-def main():
- parser, opts, args = parseargs()
- initialize(opts.config)
-
- mlist = config.db.list_manager.get(opts.listname)
- if mlist is None:
- parser.error(_('No such list: $opts.listname'))
-
- # Set up defaults.
- if opts.welcome_msg is None:
- send_welcome_msg = mlist.send_welcome_msg
- else:
- send_welcome_msg = opts.welcome_msg
- if opts.admin_notify is None:
- admin_notify = mlist.admin_notify_mchanges
- else:
- admin_notify = opts.admin_notify
-
- # Parse the csv files.
- member_data = {}
- for filename in args:
- member_data.update(parse_file(filename))
-
- future_members = set(member_data)
- current_members = set(obj.address for obj in mlist.members.addresses)
- add_members = future_members - current_members
- delete_members = current_members - future_members
- change_members = current_members & future_members
-
- with _.using(mlist.preferred_language):
- # Start by removing all the delete members.
- for address in delete_members:
- print _('deleting address: $address')
- member = mlist.members.get_member(address)
- member.unsubscribe()
- # For all members that are in both lists, update their full name and
- # delivery mode.
- for address in change_members:
- print _('updating address: $address')
- real_name, delivery_mode = member_data[address]
- member = mlist.members.get_member(address)
- member.preferences.delivery_mode = delivery_mode
- user = getUtility(IUserManager).get_user(address)
- user.real_name = real_name
- for address in add_members:
- print _('adding address: $address')
- real_name, delivery_mode = member_data[address]
- password = passwords.make_secret(
- Utils.MakeRandomPassword(),
- passwords.lookup_scheme(config.PASSWORD_SCHEME))
- add_member(mlist, address, real_name, password, delivery_mode,
- mlist.preferred_language, send_welcome_msg,
- admin_notify)
- if send_welcome_msg:
- send_welcome_message(mlist, address, language, delivery_mode)
- if admin_notify:
- send_admin_subscription_notice(mlist, address, real_name)
-
- config.db.flush()
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/src/mailman/chains/accept.py b/src/mailman/chains/accept.py
index 4b326142b..852ca8c06 100644
--- a/src/mailman/chains/accept.py
+++ b/src/mailman/chains/accept.py
@@ -22,7 +22,6 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AcceptChain',
- 'AcceptNotification',
]
@@ -30,9 +29,10 @@ import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
+from mailman.interfaces.chain import AcceptEvent
log = logging.getLogger('mailman.vette')
@@ -40,11 +40,6 @@ SEMISPACE = '; '
-class AcceptNotification(ChainNotification):
- """A notification event signaling that a message is being accepted."""
-
-
-
class AcceptChain(TerminalChainBase):
"""Accept the message for posting."""
@@ -64,4 +59,4 @@ class AcceptChain(TerminalChainBase):
msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
config.switchboards['pipeline'].enqueue(msg, msgdata)
log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
- notify(AcceptNotification(mlist, msg, msgdata, self))
+ notify(AcceptEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index 8a2e87ee2..d317bf803 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -17,18 +17,17 @@
"""Base class for terminal chains."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Chain',
- 'ChainNotification',
'Link',
'TerminalChainBase',
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.chain import (
@@ -36,9 +35,9 @@ from mailman.interfaces.chain import (
+@implementer(IChainLink)
class Link:
"""A chain link."""
- implements(IChainLink)
def __init__(self, rule, action=None, chain=None, function=None):
self.rule = rule
@@ -61,13 +60,12 @@ class Link:
+@implementer(IChain, IChainIterator)
class TerminalChainBase:
"""A base chain that always matches and executes a method.
The method is called '_process()' and must be provided by the subclass.
"""
- implements(IChain, IChainIterator)
-
def _process(self, mlist, msg, msgdata):
"""Process the message for the given mailing list.
@@ -93,9 +91,9 @@ class TerminalChainBase:
+@implementer(IMutableChain)
class Chain:
"""Generic chain base class."""
- implements(IMutableChain)
def __init__(self, name, description):
assert name not in config.chains, (
@@ -125,25 +123,13 @@ class Chain:
+@implementer(IChainIterator)
class ChainIterator:
"""Generic chain iterator."""
- implements(IChainIterator)
-
def __init__(self, chain):
self._chain = chain
def __iter__(self):
"""See `IChainIterator`."""
return self._chain.get_iterator()
-
-
-
-class ChainNotification:
- """Base class for chain notification events."""
-
- def __init__(self, mlist, msg, msgdata, chain):
- self.mlist = mlist
- self.msg = msg
- self.msgdata = msgdata
- self.chain = chain
diff --git a/src/mailman/chains/builtin.py b/src/mailman/chains/builtin.py
index 5d51e075d..7ed60dcec 100644
--- a/src/mailman/chains/builtin.py
+++ b/src/mailman/chains/builtin.py
@@ -17,7 +17,7 @@
"""The default built-in starting chain."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.chains.base import Link
from mailman.config import config
@@ -39,11 +39,10 @@ log = logging.getLogger('mailman.vette')
+@implementer(IChain)
class BuiltInChain:
"""Default built-in chain."""
- implements(IChain)
-
name = 'default-posting-chain'
description = _('The built-in moderation chain.')
diff --git a/src/mailman/chains/discard.py b/src/mailman/chains/discard.py
index 0a4bfe9ac..a52d25d7a 100644
--- a/src/mailman/chains/discard.py
+++ b/src/mailman/chains/discard.py
@@ -22,26 +22,21 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'DiscardChain',
- 'DiscardNotification',
]
import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
+from mailman.interfaces.chain import DiscardEvent
log = logging.getLogger('mailman.vette')
-class DiscardNotification(ChainNotification):
- """A notification event signaling that a message is being discarded."""
-
-
-
class DiscardChain(TerminalChainBase):
"""Discard a message."""
@@ -55,5 +50,5 @@ class DiscardChain(TerminalChainBase):
message away.
"""
log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
- notify(DiscardNotification(mlist, msg, msgdata, self))
+ notify(DiscardEvent(mlist, msg, msgdata, self))
# Nothing more needs to happen.
diff --git a/src/mailman/chains/docs/moderation.rst b/src/mailman/chains/docs/moderation.rst
index fed120147..1e968ee68 100644
--- a/src/mailman/chains/docs/moderation.rst
+++ b/src/mailman/chains/docs/moderation.rst
@@ -45,9 +45,9 @@ In order to find out whether the message is held or accepted, we can subscribe
to Zope events that are triggered on each case.
::
- >>> from mailman.chains.base import ChainNotification
+ >>> from mailman.interfaces.chain import ChainEvent
>>> def on_chain(event):
- ... if isinstance(event, ChainNotification):
+ ... if isinstance(event, ChainEvent):
... print event
... print event.chain
... print 'Subject:', event.msg['subject']
@@ -74,7 +74,7 @@ built-in chain. No rules hit and so the message is accepted.
>>> from mailman.testing.helpers import event_subscribers
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.accept.AcceptNotification ...>
+ <mailman.interfaces.chain.AcceptEvent ...>
<mailman.chains.accept.AcceptChain ...>
Subject: aardvark
Hits:
@@ -109,7 +109,7 @@ moderator approval.
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
Subject: badger
Hits:
@@ -134,7 +134,7 @@ The list's member moderation action can also be set to `discard`...
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.discard.DiscardNotification ...>
+ <mailman.interfaces.chain.DiscardEvent ...>
<mailman.chains.discard.DiscardChain ...>
Subject: cougar
Hits:
@@ -158,7 +158,7 @@ The list's member moderation action can also be set to `discard`...
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.reject.RejectNotification ...>
+ <mailman.interfaces.chain.RejectEvent ...>
<mailman.chains.reject.RejectChain ...>
Subject: dingo
Hits:
@@ -197,7 +197,7 @@ moderator approval.
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
Subject: elephant
Hits:
diff --git a/src/mailman/chains/headers.py b/src/mailman/chains/headers.py
index d9f8356f8..2dafa07f0 100644
--- a/src/mailman/chains/headers.py
+++ b/src/mailman/chains/headers.py
@@ -17,7 +17,7 @@
"""The header-matching chain."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import re
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.chains.base import Chain, Link
from mailman.config import config
@@ -60,9 +60,9 @@ def make_link(header, pattern):
+@implementer(IRule)
class HeaderMatchRule:
"""Header matching rule used by header-match chain."""
- implements(IRule)
# Sequential rule counter.
_count = 1
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index f095bc182..34389f82e 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -17,12 +17,11 @@
"""The terminal 'hold' chain."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'HoldChain',
- 'HoldNotification',
]
@@ -33,15 +32,16 @@ from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from zope.component import getUtility
from zope.event import notify
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.autorespond import IAutoResponseSet, Response
+from mailman.interfaces.chain import HoldEvent
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.usermanager import IUserManager
@@ -54,15 +54,11 @@ SEMISPACE = '; '
+@implementer(IPendable)
class HeldMessagePendable(dict):
- implements(IPendable)
PEND_KEY = 'held message'
-class HoldNotification(ChainNotification):
- """A notification event signaling that a message is being held."""
-
-
def autorespond_to_sender(mlist, sender, language=None):
"""Should Mailman automatically respond to this sender?
@@ -249,4 +245,4 @@ also appear in the first line of the body of the reply.""")),
log.info('HOLD: %s post from %s held, message-id=%s: %s',
mlist.fqdn_listname, msg.sender,
msg.get('message-id', 'n/a'), reason)
- notify(HoldNotification(mlist, msg, msgdata, self))
+ notify(HoldEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/moderation.py b/src/mailman/chains/moderation.py
index 9f9633346..6c2ed1180 100644
--- a/src/mailman/chains/moderation.py
+++ b/src/mailman/chains/moderation.py
@@ -34,7 +34,7 @@ made as to the disposition of the message. `defer` is the default for
members, while `hold` is the default for nonmembers.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -42,7 +42,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.chains.base import Link
from mailman.config import config
@@ -52,15 +52,13 @@ from mailman.interfaces.chain import IChain, LinkAction
+@implementer(IChain)
class ModerationChain:
"""Dynamically produce a link jumping to the appropriate terminal chain.
The terminal chain will be one of the Accept, Hold, Discard, or Reject
chains, based on the member's or nonmember's moderation action setting.
"""
-
- implements(IChain)
-
name = 'moderation'
description = _('Moderation chain')
diff --git a/src/mailman/chains/owner.py b/src/mailman/chains/owner.py
index ad0a04cea..22ed4b996 100644
--- a/src/mailman/chains/owner.py
+++ b/src/mailman/chains/owner.py
@@ -29,20 +29,16 @@ import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
+from mailman.interfaces.chain import AcceptOwnerEvent
log = logging.getLogger('mailman.vette')
-class OwnerNotification(ChainNotification):
- """An event signaling that a message is accepted to the -owner address."""
-
-
-
class BuiltInOwnerChain(TerminalChainBase):
"""Default built-in -owner address chain."""
@@ -53,4 +49,4 @@ class BuiltInOwnerChain(TerminalChainBase):
# At least for now, everything posted to -owners goes through.
config.switchboards['pipeline'].enqueue(msg, msgdata)
log.info('OWNER: %s', msg.get('message-id', 'n/a'))
- notify(OwnerNotification(mlist, msg, msgdata, self))
+ notify(AcceptOwnerEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index cf1b86fb2..3adc69e3a 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -22,16 +22,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'RejectChain',
- 'RejectNotification',
]
import logging
+
from zope.event import notify
from mailman.app.bounces import bounce_message
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
+from mailman.interfaces.chain import RejectEvent
log = logging.getLogger('mailman.vette')
@@ -39,11 +40,6 @@ SEMISPACE = '; '
-class RejectNotification(ChainNotification):
- """A notification event signaling that a message is being rejected."""
-
-
-
class RejectChain(TerminalChainBase):
"""Reject/bounce a message."""
@@ -64,4 +60,4 @@ class RejectChain(TerminalChainBase):
# XXX Exception/reason
bounce_message(mlist, msg)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
- notify(RejectNotification(mlist, msg, msgdata, self))
+ notify(RejectEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index f2cd1dabf..515894505 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -30,10 +30,9 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.chains.hold import autorespond_to_sender
-from mailman.config import config
from mailman.interfaces.autorespond import IAutoResponseSet, Response
from mailman.interfaces.usermanager import IUserManager
-from mailman.testing.helpers import get_queue_messages
+from mailman.testing.helpers import configuration, get_queue_messages
from mailman.testing.layers import ConfigLayer
@@ -49,15 +48,12 @@ class TestAutorespond(unittest.TestCase):
self.maxDiff = None
self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
+ @configuration('mta', max_autoresponses_per_day=1)
def test_max_autoresponses_per_day(self):
# The last one we sent was the last one we should send today. Instead
# of sending an automatic response, send them the "no more today"
- # message.
- config.push('max-1', """
- [mta]
- max_autoresponses_per_day: 1
- """)
- # Simulate a response having been sent to an address already.
+ # message. Start by simulating a response having been sent to an
+ # address already.
anne = getUtility(IUserManager).create_address('anne@example.com')
response_set = IAutoResponseSet(self._mlist)
response_set.response_sent(anne, Response.hold)
diff --git a/src/mailman/chains/tests/test_owner.py b/src/mailman/chains/tests/test_owner.py
index db85d4967..b50c08e31 100644
--- a/src/mailman/chains/tests/test_owner.py
+++ b/src/mailman/chains/tests/test_owner.py
@@ -28,8 +28,9 @@ __all__ = [
import unittest
from mailman.app.lifecycle import create_list
-from mailman.chains.owner import BuiltInOwnerChain, OwnerNotification
+from mailman.chains.owner import BuiltInOwnerChain
from mailman.core.chains import process
+from mailman.interfaces.chain import AcceptOwnerEvent
from mailman.testing.helpers import (
event_subscribers,
get_queue_messages,
@@ -60,12 +61,13 @@ Message-ID: <ant>
# is processed by the owner chain.
events = []
def catch_event(event):
- events.append(event)
+ if isinstance(event, AcceptOwnerEvent):
+ events.append(event)
with event_subscribers(catch_event):
process(self._mlist, self._msg, {}, 'default-owner-chain')
self.assertEqual(len(events), 1)
event = events[0]
- self.assertTrue(isinstance(event, OwnerNotification))
+ self.assertTrue(isinstance(event, AcceptOwnerEvent))
self.assertEqual(event.mlist, self._mlist)
self.assertEqual(event.msg['message-id'], '<ant>')
self.assertTrue(isinstance(event.chain, BuiltInOwnerChain))
diff --git a/src/mailman/commands/cli_aliases.py b/src/mailman/commands/cli_aliases.py
index 7c1577c9c..d692ba356 100644
--- a/src/mailman/commands/cli_aliases.py
+++ b/src/mailman/commands/cli_aliases.py
@@ -17,7 +17,7 @@
"""Generate Mailman alias files for your MTA."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,7 +29,7 @@ import sys
from operator import attrgetter
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -41,11 +41,10 @@ from mailman.utilities.modules import call_name
+@implementer(ICLISubCommand)
class Aliases:
"""Regenerate the aliases appropriate for your MTA."""
- implements(ICLISubCommand)
-
name = 'aliases'
def add(self, parser, command_parser):
@@ -91,11 +90,10 @@ class Aliases:
+@implementer(IMailTransportAgentLifecycle)
class Dummy:
"""Dummy aliases implementation for simpler output format."""
- implements(IMailTransportAgentLifecycle)
-
def create(self, mlist):
"""See `IMailTransportAgentLifecycle`."""
raise NotImplementedError
@@ -132,5 +130,5 @@ class Dummy:
for mlist in sorted(by_domain[domain], key=sort_key):
utility = getUtility(IMailTransportAgentAliases)
for alias in utility.aliases(mlist):
- print >> fp, alias
- print >> fp
+ print(alias, file=fp)
+ print(file=fp)
diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py
index 2013d6745..8349feb60 100644
--- a/src/mailman/commands/cli_control.py
+++ b/src/mailman/commands/cli_control.py
@@ -17,7 +17,7 @@
"""Module stuff."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -34,7 +34,7 @@ import errno
import signal
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.bin.master import WatcherState, master_state
from mailman.config import config
@@ -46,11 +46,10 @@ qlog = logging.getLogger('mailman.runner')
+@implementer(ICLISubCommand)
class Start:
"""Start the Mailman daemons."""
- implements(ICLISubCommand)
-
name = 'start'
def add(self, parser, command_parser):
@@ -107,7 +106,7 @@ class Start:
'cleanly. Try using --force.'))
def log(message):
if not args.quiet:
- print message
+ print(message)
# Daemon process startup according to Stevens, Advanced Programming in
# the UNIX Environment, Chapter 13.
pid = os.fork()
@@ -147,27 +146,26 @@ def kill_watcher(sig):
pid = int(fp.read().strip())
except (IOError, ValueError) as error:
# For i18n convenience
- print >> sys.stderr, _('PID unreadable in: $config.PID_FILE')
- print >> sys.stderr, error
- print >> sys.stderr, _('Is the master even running?')
+ print(_('PID unreadable in: $config.PID_FILE'), file=sys.stderr)
+ print(error, file=sys.stderr)
+ print(_('Is the master even running?'), file=sys.stderr)
return
try:
os.kill(pid, sig)
except OSError as error:
if error.errno != errno.ESRCH:
raise
- print >> sys.stderr, _('No child with pid: $pid')
- print >> sys.stderr, error
- print >> sys.stderr, _('Stale pid file removed.')
+ print(_('No child with pid: $pid'), file=sys.stderr)
+ print(error, file=sys.stderr)
+ print(_('Stale pid file removed.'), file=sys.stderr)
os.unlink(config.PID_FILE)
+@implementer(ICLISubCommand)
class SignalCommand:
"""Common base class for simple, signal sending commands."""
- implements(ICLISubCommand)
-
name = None
message = None
signal = None
@@ -184,7 +182,7 @@ class SignalCommand:
def process(self, args):
"""See `ICLISubCommand`."""
if not args.quiet:
- print _(self.message)
+ print(_(self.message))
kill_watcher(self.signal)
@@ -204,11 +202,10 @@ class Reopen(SignalCommand):
signal = signal.SIGHUP
+@implementer(ICLISubCommand)
class Restart(SignalCommand):
"""Stop the Mailman daemons."""
- implements(ICLISubCommand)
-
name = 'restart'
message = _('Restarting the Mailman runners')
signal = signal.SIGUSR1
diff --git a/src/mailman/commands/cli_help.py b/src/mailman/commands/cli_help.py
index 538d9c520..a85dcd442 100644
--- a/src/mailman/commands/cli_help.py
+++ b/src/mailman/commands/cli_help.py
@@ -17,7 +17,7 @@
"""The 'help' subcommand."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,18 +25,17 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.interfaces.command import ICLISubCommand
+@implementer(ICLISubCommand)
class Help:
# Lowercase, to match argparse's default --help text.
"""show this help message and exit"""
- implements(ICLISubCommand)
-
name = 'help'
def add(self, parser, command_parser):
diff --git a/src/mailman/commands/cli_import.py b/src/mailman/commands/cli_import.py
index b703f3ffd..f6c016585 100644
--- a/src/mailman/commands/cli_import.py
+++ b/src/mailman/commands/cli_import.py
@@ -17,7 +17,7 @@
"""Importing list data into Mailman 3."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,21 +29,20 @@ import sys
import cPickle
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.core.i18n import _
+from mailman.database.transaction import transactional
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
from mailman.utilities.importer import import_config_pck
+@implementer(ICLISubCommand)
class Import21:
"""Import Mailman 2.1 list data."""
- implements(ICLISubCommand)
-
name = 'import21'
def add(self, parser, command_parser):
@@ -59,6 +58,7 @@ class Import21:
'pickle_file', metavar='FILENAME', nargs=1,
help=_('The path to the config.pck file to import.'))
+ @transactional
def process(self, args):
"""See `ICLISubCommand`."""
# Could be None or sequence of length 0.
@@ -90,10 +90,7 @@ class Import21:
return
else:
if not isinstance(config_dict, dict):
- print >> sys.stderr, _(
- 'Ignoring non-dictionary: {0!r}').format(
- config_dict)
+ print(_('Ignoring non-dictionary: {0!r}').format(
+ config_dict), file=sys.stderr)
continue
import_config_pck(mlist, config_dict)
- # Commit the changes to the database.
- config.db.commit()
diff --git a/src/mailman/commands/cli_info.py b/src/mailman/commands/cli_info.py
index 24ccec4fb..cd7269404 100644
--- a/src/mailman/commands/cli_info.py
+++ b/src/mailman/commands/cli_info.py
@@ -17,7 +17,7 @@
"""Information about this Mailman instance."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,8 @@ __all__ = [
import sys
-from zope.interface import implements
+from lazr.config import as_boolean
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -37,11 +38,10 @@ from mailman.version import MAILMAN_VERSION_FULL
+@implementer(ICLISubCommand)
class Info:
"""Information about this Mailman instance."""
- implements(ICLISubCommand)
-
name = 'info'
def add(self, parser, command_parser):
@@ -65,15 +65,19 @@ class Info:
# We don't need to close output because that will happen
# automatically when the script exits.
output = open(args.output, 'w')
- print >> output, MAILMAN_VERSION_FULL
- print >> output, 'Python', sys.version
- print >> output, 'config file:', config.filename
- print >> output, 'db url:', config.db.url
- print >> output, 'REST root url:', path_to('/')
- print >> output, 'REST credentials: {0}:{1}'.format(
- config.webservice.admin_user, config.webservice.admin_pass)
+ print(MAILMAN_VERSION_FULL, file=output)
+ print('Python', sys.version, file=output)
+ print('config file:', config.filename, file=output)
+ print('db url:', config.db.url, file=output)
+ print('devmode:',
+ 'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED',
+ file=output)
+ print('REST root url:', path_to('/'), file=output)
+ print('REST credentials: {0}:{1}'.format(
+ config.webservice.admin_user, config.webservice.admin_pass),
+ file=output)
if args.verbose:
- print >> output, 'File system paths:'
+ print('File system paths:', file=output)
longest = 0
paths = {}
for attribute in dir(config):
@@ -81,5 +85,5 @@ class Info:
paths[attribute] = getattr(config, attribute)
longest = max(longest, len(attribute))
for attribute in sorted(paths):
- print ' {0:{2}} = {1}'.format(attribute, paths[attribute],
- longest)
+ print(' {0:{2}} = {1}'.format(
+ attribute, paths[attribute], longest))
diff --git a/src/mailman/commands/cli_inject.py b/src/mailman/commands/cli_inject.py
index 321a92c78..1434fd2a6 100644
--- a/src/mailman/commands/cli_inject.py
+++ b/src/mailman/commands/cli_inject.py
@@ -17,7 +17,7 @@
"""bin/mailman inject"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import sys
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.inject import inject_text
from mailman.config import config
@@ -38,11 +38,10 @@ from mailman.interfaces.listmanager import IListManager
+@implementer(ICLISubCommand)
class Inject:
"""Inject a message from a file into a mailing list's queue."""
- implements(ICLISubCommand)
-
name = 'inject'
def add(self, parser, command_parser):
@@ -82,9 +81,9 @@ class Inject:
# Process --show first; if given, print output and exit, ignoring all
# other command line switches.
if args.show:
- print 'Available queues:'
+ print('Available queues:')
for switchboard in sorted(config.switchboards):
- print ' ', switchboard
+ print(' ', switchboard)
return
# Could be None or sequence of length 0.
if args.listname is None:
@@ -106,7 +105,7 @@ class Inject:
try:
message_text = sys.stdin.read()
except KeyboardInterrupt:
- print 'Interrupted'
+ print('Interrupted')
sys.exit(1)
else:
with open(args.filename) as fp:
diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py
index af6afe22d..b91f708de 100644
--- a/src/mailman/commands/cli_lists.py
+++ b/src/mailman/commands/cli_lists.py
@@ -17,7 +17,7 @@
"""The 'lists' subcommand."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,12 +28,12 @@ __all__ = [
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.lifecycle import create_list, remove_list
-from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.core.i18n import _
+from mailman.database.transaction import transaction, transactional
from mailman.email.message import UserNotification
from mailman.interfaces.address import (
IEmailValidator, InvalidEmailAddressError)
@@ -49,11 +49,10 @@ COMMASPACE = ', '
+@implementer(ICLISubCommand)
class Lists:
"""List all mailing lists"""
- implements(ICLISubCommand)
-
name = 'lists'
def add(self, parser, command_parser):
@@ -98,11 +97,11 @@ class Lists:
# Maybe no mailing lists matched.
if len(mailing_lists) == 0:
if not args.quiet:
- print _('No matching mailing lists found')
+ print(_('No matching mailing lists found'))
return
count = len(mailing_lists)
if not args.quiet:
- print _('$count matching mailing lists found:')
+ print(_('$count matching mailing lists found:'))
# Calculate the longest identifier.
longest = 0
output = []
@@ -120,16 +119,15 @@ class Lists:
else:
format_string = '{0:{2}}'
for identifier, description in output:
- print format_string.format(
- identifier, description, longest, 70 - longest)
+ print(format_string.format(
+ identifier, description, longest, 70 - longest))
+@implementer(ICLISubCommand)
class Create:
"""Create a mailing list"""
- implements(ICLISubCommand)
-
name = 'create'
def add(self, parser, command_parser):
@@ -214,13 +212,13 @@ class Create:
self.parser.error(_('Undefined domain: $domain'))
return
# Find the language associated with the code, then set the mailing
- # list's preferred language to that. The changes then must be
- # committed to the database.
- mlist.preferred_language = getUtility(ILanguageManager)[language_code]
- config.db.commit()
+ # list's preferred language to that.
+ language_manager = getUtility(ILanguageManager)
+ with transaction():
+ mlist.preferred_language = language_manager[language_code]
# Do the notification.
if not args.quiet:
- print _('Created mailing list: $mlist.fqdn_listname')
+ print(_('Created mailing list: $mlist.fqdn_listname'))
if args.notify:
d = dict(
listname = mlist.fqdn_listname,
@@ -242,11 +240,10 @@ class Create:
+@implementer(ICLISubCommand)
class Remove:
"""Remove a mailing list"""
- implements(ICLISubCommand)
-
name = 'remove'
def add(self, parser, command_parser):
@@ -262,11 +259,12 @@ class Remove:
The 'fully qualified list name', i.e. the posting address of the
mailing list."""))
+ @transactional
def process(self, args):
"""See `ICLISubCommand`."""
def log(message):
if not args.quiet:
- print message
+ print(message)
assert len(args.listname) == 1, (
'Unexpected positional arguments: %s' % args.listname)
fqdn_listname = args.listname[0]
@@ -276,5 +274,4 @@ class Remove:
return
else:
log(_('Removed list: $fqdn_listname'))
- remove_list(fqdn_listname, mlist)
- config.db.commit()
+ remove_list(mlist)
diff --git a/src/mailman/commands/cli_members.py b/src/mailman/commands/cli_members.py
index 2bf6be848..7c5d3b8f3 100644
--- a/src/mailman/commands/cli_members.py
+++ b/src/mailman/commands/cli_members.py
@@ -29,14 +29,15 @@ import sys
import codecs
from email.utils import formataddr, parseaddr
-from flufl.password import generate
from operator import attrgetter
+from passlib.utils import generate_password as generate
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.membership import add_member
from mailman.config import config
from mailman.core.i18n import _
+from mailman.database.transaction import transactional
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.member import (
@@ -44,11 +45,10 @@ from mailman.interfaces.member import (
+@implementer(ICLISubCommand)
class Members:
"""Manage list memberships. With no arguments, list all members."""
- implements(ICLISubCommand)
-
name = 'members'
def add(self, parser, command_parser):
@@ -177,6 +177,7 @@ class Members:
if fp is not sys.stdout:
fp.close()
+ @transactional
def add_members(self, mlist, args):
"""Add the members in a file to a mailing list.
@@ -207,9 +208,8 @@ class Members:
except AlreadySubscribedError:
# It's okay if the address is already subscribed, just
# print a warning and continue.
- print('Already subscribed (skipping):',
+ print('Already subscribed (skipping):',
email, display_name)
finally:
if fp is not sys.stdin:
fp.close()
- config.db.commit()
diff --git a/src/mailman/commands/cli_qfile.py b/src/mailman/commands/cli_qfile.py
index 78156f08c..b9e0eff02 100644
--- a/src/mailman/commands/cli_qfile.py
+++ b/src/mailman/commands/cli_qfile.py
@@ -17,7 +17,7 @@
"""Getting information out of a qfile."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import cPickle
from pprint import PrettyPrinter
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.command import ICLISubCommand
@@ -39,11 +39,10 @@ m = []
+@implementer(ICLISubCommand)
class QFile:
"""Get information out of a queue file."""
- implements(ICLISubCommand)
-
name = 'qfile'
def add(self, parser, command_parser):
@@ -79,15 +78,15 @@ class QFile:
except EOFError:
break
if args.doprint:
- print _('[----- start pickle -----]')
+ print(_('[----- start pickle -----]'))
for i, obj in enumerate(m):
count = i + 1
- print _('<----- start object $count ----->')
+ print(_('<----- start object $count ----->'))
if isinstance(obj, basestring):
- print obj
+ print(obj)
else:
printer.pprint(obj)
- print _('[----- end pickle -----]')
+ print(_('[----- end pickle -----]'))
count = len(m)
banner = _("The variable 'm' contains $count objects")
if args.interactive:
diff --git a/src/mailman/commands/cli_status.py b/src/mailman/commands/cli_status.py
index 14b0d976b..9cbaa4f22 100644
--- a/src/mailman/commands/cli_status.py
+++ b/src/mailman/commands/cli_status.py
@@ -15,9 +15,9 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""bin/mailman status."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
import socket
-from zope.interface import implements
+from zope.interface import implementer
from mailman.bin.master import WatcherState, master_state
from mailman.core.i18n import _
@@ -35,11 +35,10 @@ from mailman.interfaces.command import ICLISubCommand
+@implementer(ICLISubCommand)
class Status:
"""Status of the Mailman system."""
- implements(ICLISubCommand)
-
name = 'status'
def add(self, parser, command_parser):
@@ -64,5 +63,5 @@ class Status:
'Invalid enum value: %s' % status)
message = _('GNU Mailman is in an unexpected state '
'($hostname != $fqdn_name)')
- print message
+ print(message)
return int(status)
diff --git a/src/mailman/commands/cli_unshunt.py b/src/mailman/commands/cli_unshunt.py
index 4ce711b83..bc50d95ef 100644
--- a/src/mailman/commands/cli_unshunt.py
+++ b/src/mailman/commands/cli_unshunt.py
@@ -17,7 +17,7 @@
"""The 'unshunt' command."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
import sys
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -35,11 +35,10 @@ from mailman.interfaces.command import ICLISubCommand
+@implementer(ICLISubCommand)
class Unshunt:
"""Unshunt messages."""
- implements(ICLISubCommand)
-
name = 'unshunt'
def add(self, parser, command_parser):
@@ -64,8 +63,8 @@ class Unshunt:
if not args.discard:
config.switchboards[which_queue].enqueue(msg, msgdata)
except Exception as error:
- print >> sys.stderr, _(
- 'Cannot unshunt message $filebase, skipping:\n$error')
+ print(_('Cannot unshunt message $filebase, skipping:\n$error'),
+ file=sys.stderr)
else:
# Unlink the .bak file left by dequeue()
shunt_queue.finish(filebase)
diff --git a/src/mailman/commands/cli_version.py b/src/mailman/commands/cli_version.py
index 4090b1173..b5e9b65ff 100644
--- a/src/mailman/commands/cli_version.py
+++ b/src/mailman/commands/cli_version.py
@@ -17,7 +17,7 @@
"""The Mailman version."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,18 +25,17 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.interfaces.command import ICLISubCommand
from mailman.version import MAILMAN_VERSION_FULL
+@implementer(ICLISubCommand)
class Version:
"""Mailman's version."""
- implements(ICLISubCommand)
-
name = 'version'
def add(self, parser, command_parser):
@@ -46,4 +45,4 @@ class Version:
def process(self, args):
"""See `ICLISubCommand`."""
- print MAILMAN_VERSION_FULL
+ print(MAILMAN_VERSION_FULL)
diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py
index e514c798f..4ccdd8798 100644
--- a/src/mailman/commands/cli_withlist.py
+++ b/src/mailman/commands/cli_withlist.py
@@ -17,7 +17,7 @@
"""bin/mailman withlist"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,7 +31,7 @@ import sys
from lazr.config import as_boolean
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -47,14 +47,13 @@ r = None
+@implementer(ICLISubCommand)
class Withlist:
"""Operate on a mailing list.
For detailed help, see --details
"""
- implements(ICLISubCommand)
-
name = 'withlist'
def add(self, parser, command_parser):
@@ -163,7 +162,7 @@ class Withlist:
ipshell = InteractiveShellEmbed(banner1=banner, user_ns=overrides)
ipshell()
except ImportError:
- print _('ipython is not available, set use_ipython to no')
+ print(_('ipython is not available, set use_ipython to no'))
def _start_python(self, overrides, banner):
# Set the tab completion.
@@ -178,25 +177,25 @@ class Withlist:
def _details(self):
"""Print detailed usage."""
# Split this up into paragraphs for easier translation.
- print _("""\
+ print(_("""\
This script provides you with a general framework for interacting with a
-mailing list.""")
- print
- print _("""\
+mailing list."""))
+ print()
+ print(_("""\
There are two ways to use this script: interactively or programmatically.
Using it interactively allows you to play with, examine and modify a mailing
list from Python's interactive interpreter. When running interactively, the
variable 'm' will be available in the global namespace. It will reference the
-mailing list object.""")
- print
- print _("""\
+mailing list object."""))
+ print()
+ print(_("""\
Programmatically, you can write a function to operate on a mailing list, and
this script will take care of the housekeeping (see below for examples). In
that case, the general usage syntax is:
- % bin/mailman withlist [options] listname [args ...]""")
- print
- print _("""\
+ % bin/mailman withlist [options] listname [args ...]"""))
+ print()
+ print(_("""\
Here's an example of how to use the --run option. Say you have a file in the
Mailman installation directory called 'listaddr.py', with the following two
functions:
@@ -205,26 +204,26 @@ functions:
print mlist.posting_address
def requestaddr(mlist):
- print mlist.request_address""")
- print
- print _("""\
+ print mlist.request_address"""))
+ print()
+ print(_("""\
You can print the list's posting address by running the following from the
command line:
% bin/mailman withlist -r listaddr mylist@example.com
Importing listaddr ...
Running listaddr.listaddr() ...
- mylist@example.com""")
- print
- print _("""\
+ mylist@example.com"""))
+ print()
+ print(_("""\
And you can print the list's request address by running:
% bin/mailman withlist -r listaddr.requestaddr mylist
Importing listaddr ...
Running listaddr.requestaddr() ...
- mylist-request@example.com""")
- print
- print _("""\
+ mylist-request@example.com"""))
+ print()
+ print(_("""\
As another example, say you wanted to change the display name for a particular
mailing list. You could put the following function in a file called
'change.pw':
@@ -236,7 +235,7 @@ mailing list. You could put the following function in a file called
and run this from the command line:
- % bin/mailman withlist -r change mylist@example.com 'My List'""")
+ % bin/mailman withlist -r change mylist@example.com 'My List'"""))
diff --git a/src/mailman/commands/docs/end.rst b/src/mailman/commands/docs/end.rst
index accf91b90..8cd4b2409 100644
--- a/src/mailman/commands/docs/end.rst
+++ b/src/mailman/commands/docs/end.rst
@@ -13,8 +13,8 @@ processing email messages.
The 'end' command takes no arguments.
- >>> command.argument_description
- ''
+ >>> print 'DESCRIPTION:', command.argument_description
+ DESCRIPTION:
The command itself is fairly simple; it just stops command processing, and the
message isn't even looked at.
@@ -31,7 +31,7 @@ The 'stop' command is a synonym for 'end'.
stop
>>> print command.description
An alias for 'end'.
- >>> command.argument_description
- ''
+ >>> print 'DESCRIPTION:', command.argument_description
+ DESCRIPTION:
>>> print command.process(mlist, Message(), {}, (), None)
ContinueProcessing.no
diff --git a/src/mailman/commands/docs/info.rst b/src/mailman/commands/docs/info.rst
index 7f69eada5..59801f234 100644
--- a/src/mailman/commands/docs/info.rst
+++ b/src/mailman/commands/docs/info.rst
@@ -37,6 +37,7 @@ By passing in the ``-o/--output`` option, you can print the info to a file.
...
config file: .../test.cfg
db url: ...
+ devmode: DISABLED
REST root url: http://localhost:9001/3.0/
REST credentials: restadmin:restpass
@@ -50,6 +51,7 @@ system paths that Mailman is using.
... [mailman]
... layout: fhs
... """)
+ >>> cleanups.append((config.pop, 'fhs'))
>>> config.create_paths = True
The File System Hierarchy layout is the same every by definition.
@@ -73,9 +75,3 @@ The File System Hierarchy layout is the same every by definition.
QUEUE_DIR = /var/spool/mailman
TEMPLATE_DIR = .../mailman/templates
VAR_DIR = /var/lib/mailman
-
-
-Clean up
-========
-
- >>> config.pop('fhs')
diff --git a/src/mailman/commands/eml_confirm.py b/src/mailman/commands/eml_confirm.py
index 55619a503..c82dc64c3 100644
--- a/src/mailman/commands/eml_confirm.py
+++ b/src/mailman/commands/eml_confirm.py
@@ -17,7 +17,7 @@
"""Module stuff."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,7 +26,7 @@ __all__ = [
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
@@ -34,11 +34,10 @@ from mailman.interfaces.registrar import IRegistrar
+@implementer(IEmailCommand)
class Confirm:
"""The email 'confirm' command."""
- implements(IEmailCommand)
-
name = 'confirm'
argument_description = 'token'
description = _('Confirm a subscription request.')
@@ -48,7 +47,7 @@ class Confirm:
"""See `IEmailCommand`."""
# The token must be in the arguments.
if len(arguments) == 0:
- print >> results, _('No confirmation token found')
+ print(_('No confirmation token found'), file=results)
return ContinueProcessing.no
# Make sure we don't try to confirm the same token more than once.
token = arguments[0]
@@ -60,7 +59,7 @@ class Confirm:
results.confirms = tokens
succeeded = getUtility(IRegistrar).confirm(token)
if succeeded:
- print >> results, _('Confirmed')
+ print(_('Confirmed'), file=results)
return ContinueProcessing.yes
- print >> results, _('Confirmation token did not match')
+ print(_('Confirmation token did not match'), file=results)
return ContinueProcessing.no
diff --git a/src/mailman/commands/eml_echo.py b/src/mailman/commands/eml_echo.py
index 06d5ee5e7..885edcbae 100644
--- a/src/mailman/commands/eml_echo.py
+++ b/src/mailman/commands/eml_echo.py
@@ -25,7 +25,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
@@ -35,9 +35,9 @@ SPACE = ' '
+@implementer(IEmailCommand)
class Echo:
"""The email 'echo' command."""
- implements(IEmailCommand)
name = 'echo'
argument_description = '[args]'
diff --git a/src/mailman/commands/eml_end.py b/src/mailman/commands/eml_end.py
index 3cd70813c..32a024205 100644
--- a/src/mailman/commands/eml_end.py
+++ b/src/mailman/commands/eml_end.py
@@ -17,6 +17,8 @@
"""The email commands 'end' and 'stop'."""
+from __future__ import absolute_import, print_function, unicode_literals
+
__metaclass__ = type
__all__ = [
'End',
@@ -24,16 +26,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+@implementer(IEmailCommand)
class End:
"""The email 'end' command."""
- implements(IEmailCommand)
name = 'end'
argument_description = ''
diff --git a/src/mailman/commands/eml_help.py b/src/mailman/commands/eml_help.py
index 6fddb4ef3..a27717179 100644
--- a/src/mailman/commands/eml_help.py
+++ b/src/mailman/commands/eml_help.py
@@ -25,7 +25,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -37,11 +37,10 @@ SPACE = ' '
+@implementer(IEmailCommand)
class Help:
"""The email 'help' command."""
- implements(IEmailCommand)
-
name = 'help'
argument_description = '[command]'
description = _('Get help about available email commands.')
@@ -58,14 +57,14 @@ class Help:
command = config.commands[command_name]
short_description = getattr(
command, 'short_description', _('n/a'))
- print(format.format(command.name, short_description),
+ print(format.format(command.name, short_description),
file=results)
return ContinueProcessing.yes
elif len(arguments) == 1:
command_name = arguments[0]
command = config.commands.get(command_name)
if command is None:
- print(_('$self.name: no such command: $command_name'),
+ print(_('$self.name: no such command: $command_name'),
file=results)
return ContinueProcessing.no
print('{0} {1}'.format(command.name, command.argument_description),
diff --git a/src/mailman/commands/eml_membership.py b/src/mailman/commands/eml_membership.py
index d6f7a47d9..63efbafca 100644
--- a/src/mailman/commands/eml_membership.py
+++ b/src/mailman/commands/eml_membership.py
@@ -30,7 +30,7 @@ __all__ = [
from email.utils import formataddr, parseaddr
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
@@ -41,11 +41,10 @@ from mailman.interfaces.usermanager import IUserManager
+@implementer(IEmailCommand)
class Join:
"""The email 'join' command."""
- implements(IEmailCommand)
-
name = 'join'
# XXX 2012-02-29 BAW: DeliveryMode.summary is not yet supported.
argument_description = '[digest=<no|mime|plain>]'
@@ -85,11 +84,11 @@ used.
# Is this person already a member of the list? Search for all
# matching memberships.
members = getUtility(ISubscriptionService).find_members(
- address, mlist.fqdn_listname, MemberRole.member)
+ address, mlist.list_id, MemberRole.member)
if len(members) > 0:
print(_('$person is already a member'), file=results)
else:
- getUtility(IRegistrar).register(mlist, address,
+ getUtility(IRegistrar).register(mlist, address,
display_name, delivery_mode)
print(_('Confirmation email sent to $person'), file=results)
return ContinueProcessing.yes
@@ -131,14 +130,13 @@ class Subscribe(Join):
+@implementer(IEmailCommand)
class Leave:
"""The email 'leave' command."""
- implements(IEmailCommand)
-
name = 'leave'
argument_description = ''
- description = _("""Leave this mailing list.
+ description = _("""Leave this mailing list.
You may be asked to confirm your request.""")
short_description = _('Leave this mailing list.')
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 48c849148..f6c39fcec 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -32,13 +32,15 @@ from lazr.config import ConfigSchema, as_boolean
from pkg_resources import resource_stream
from string import Template
from zope.component import getUtility
-from zope.interface import Interface, implements
+from zope.event import notify
+from zope.interface import implementer
import mailman.templates
from mailman import version
+from mailman.interfaces.configuration import (
+ ConfigurationUpdatedEvent, IConfiguration)
from mailman.interfaces.languages import ILanguageManager
-from mailman.interfaces.styles import IStyleManager
from mailman.utilities.filesystem import makedirs
from mailman.utilities.modules import call_name
@@ -47,16 +49,10 @@ SPACE = ' '
-class IConfiguration(Interface):
- """Marker interface; used for adaptation in the REST API."""
-
-
-
+@implementer(IConfiguration)
class Configuration:
"""The core global configuration object."""
- implements(IConfiguration)
-
def __init__(self):
self.switchboards = {}
self.QFILE_SCHEMA_VERSION = version.QFILE_SCHEMA_VERSION
@@ -71,6 +67,7 @@ class Configuration:
self.handlers = {}
self.pipelines = {}
self.commands = {}
+ self.password_context = None
def _clear(self):
"""Clear the cached configuration variables."""
@@ -119,26 +116,8 @@ class Configuration:
"""Perform post-processing after loading the configuration files."""
# Expand and set up all directories.
self._expand_paths()
- # Set up the switchboards. Import this here to avoid circular imports.
- from mailman.core.switchboard import Switchboard
- Switchboard.initialize()
- # Set up all the languages.
- languages = self._config.getByCategory('language', [])
- language_manager = getUtility(ILanguageManager)
- for language in languages:
- if language.enabled:
- code = language.name.split('.')[1]
- language_manager.add(
- code, language.charset, language.description)
- # The default language must always be available.
- assert self._config.mailman.default_language in language_manager, (
- 'System default language code not defined: %s' %
- self._config.mailman.default_language)
self.ensure_directories_exist()
- getUtility(IStyleManager).populate()
- # Set the default system language.
- from mailman.core.i18n import _
- _.default = self.mailman.default_language
+ notify(ConfigurationUpdatedEvent(self))
def _expand_paths(self):
"""Expand all configuration paths."""
@@ -250,3 +229,9 @@ class Configuration:
"""Iterate over all the style configuration sections."""
for section in self._config.getByCategory('style', []):
yield section
+
+ @property
+ def language_configs(self):
+ """Iterate over all the language configuration sections."""
+ for section in self._config.getByCategory('language', []):
+ yield section
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index 8c362c31b..ed85ae1a6 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -22,6 +22,20 @@
factory="mailman.model.requests.ListRequests"
/>
+ <adapter
+ for="mailman.interfaces.database.IDatabase"
+ provides="mailman.interfaces.database.ITemporaryDatabase"
+ factory="mailman.database.sqlite.make_temporary"
+ name="sqlite"
+ />
+
+ <adapter
+ for="mailman.interfaces.database.IDatabase"
+ provides="mailman.interfaces.database.ITemporaryDatabase"
+ factory="mailman.database.postgresql.make_temporary"
+ name="postgres"
+ />
+
<utility
provides="mailman.interfaces.bans.IBanManager"
factory="mailman.model.bans.BanManager"
@@ -33,6 +47,24 @@
/>
<utility
+ provides="mailman.interfaces.database.IDatabaseFactory"
+ factory="mailman.database.factory.DatabaseFactory"
+ name="production"
+ />
+
+ <utility
+ provides="mailman.interfaces.database.IDatabaseFactory"
+ factory="mailman.database.factory.DatabaseTestingFactory"
+ name="testing"
+ />
+
+ <utility
+ provides="mailman.interfaces.database.IDatabaseFactory"
+ factory="mailman.database.factory.DatabaseTemporaryFactory"
+ name="temporary"
+ />
+
+ <utility
provides="mailman.interfaces.domain.IDomainManager"
factory="mailman.model.domain.DomainManager"
/>
diff --git a/src/mailman/config/passlib.cfg b/src/mailman/config/passlib.cfg
new file mode 100644
index 000000000..805f0fb11
--- /dev/null
+++ b/src/mailman/config/passlib.cfg
@@ -0,0 +1,10 @@
+[passlib]
+# This is the output of passlibs.apps.custom_app_context.to_string().
+# See http://packages.python.org/passlib/index.html for details.
+schemes = sha512_crypt, sha256_crypt
+default = sha512_crypt
+all__vary_rounds = 0.1
+sha256_crypt__min_rounds = 80000
+sha512_crypt__min_rounds = 60000
+admin__sha256_crypt__min_rounds = 160000
+admin__sha512_crypt__min_rounds = 120000
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 00b8d9325..e36d33c10 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -27,11 +27,10 @@
# a human.
site_owner: changeme@example.com
-# This address is used as the from address whenever a message comes from some
-# entity to which there is no natural reply recipient. Set this to a real
-# human or to /dev/null. It will be appended with the host name of the list
-# involved. This address must not bounce and it must not point to a Mailman
-# process.
+# This is the local-part of an email address used in the From field whenever a
+# message comes from some entity to which there is no natural reply recipient.
+# Mailman will append '@' and the host name of the list involved. This
+# address must not bounce and it must not point to a Mailman process.
noreply_address: noreply
# The default language for this server.
@@ -154,10 +153,13 @@ wait: 10s
[passwords]
-# The default scheme to use to encrypt new passwords. Existing passwords
-# include the scheme that was used to encrypt them, so it's okay to change
-# this after users have been added.
-password_scheme: ssha
+# Where can we find the passlib configuration file? The path can be either a
+# file system path or a Python import path. If the value starts with python:
+# then it is a Python import path, otherwise it is a file system path. File
+# system paths must be absolute since no guarantees are made about the current
+# working directory. Python paths should not include the trailing .cfg, which
+# the file must end with.
+path: python:mailman.config.passlib
# When Mailman generates them, this is the default length of passwords.
password_length: 8
diff --git a/src/mailman/config/tests/__init__.py b/src/mailman/config/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/config/tests/__init__.py
diff --git a/src/mailman/config/tests/test_configuration.py b/src/mailman/config/tests/test_configuration.py
new file mode 100644
index 000000000..88e00cbb9
--- /dev/null
+++ b/src/mailman/config/tests/test_configuration.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the system-wide global configuration."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestConfiguration',
+ ]
+
+
+import unittest
+
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+from mailman.testing.helpers import configuration, event_subscribers
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestConfiguration(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_push_and_pop_trigger_events(self):
+ # Pushing a new configuration onto the stack triggers a
+ # post-processing event.
+ events = []
+ def on_event(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ # Record both the event and the top overlay.
+ events.append(event.config.overlays[0].name)
+ with event_subscribers(on_event):
+ with configuration('test', _configname='my test'):
+ pass
+ # There should be two pushed configuration names on the list now, one
+ # for the push leaving 'my test' on the top of the stack, and one for
+ # the pop, leaving the ConfigLayer's 'test config' on top.
+ self.assertEqual(events, ['my test', 'test config'])
diff --git a/src/mailman/core/constants.py b/src/mailman/core/constants.py
index 02d46a088..4562f4c74 100644
--- a/src/mailman/core/constants.py
+++ b/src/mailman/core/constants.py
@@ -17,7 +17,7 @@
"""Various constants and enumerations."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,7 +26,7 @@ __all__ = [
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
@@ -35,11 +35,10 @@ from mailman.interfaces.preferences import IPreferences
+@implementer(IPreferences)
class SystemDefaultPreferences:
"""The default system preferences."""
- implements(IPreferences)
-
acknowledge_posts = False
hide_address = True
receive_list_copy = True
diff --git a/src/mailman/core/i18n.py b/src/mailman/core/i18n.py
index 2d7c382c7..73453ae65 100644
--- a/src/mailman/core/i18n.py
+++ b/src/mailman/core/i18n.py
@@ -31,7 +31,7 @@ import time
from flufl.i18n import PackageStrategy, registry
import mailman.messages
-
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
_ = None
@@ -113,3 +113,9 @@ def ctime(date):
wday = daysofweek[wday]
mon = months[mon]
return _('$wday $mon $day $hh:$mm:$ss $tzname $year')
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ _.default = event.config.mailman.default_language
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
index 389a45f3b..eb8787ad2 100644
--- a/src/mailman/core/initialize.py
+++ b/src/mailman/core/initialize.py
@@ -24,7 +24,7 @@ line argument parsing, since some of the initialization behavior is controlled
by the command line arguments.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -40,13 +40,13 @@ import os
import sys
from pkg_resources import resource_string
+from zope.component import getUtility
from zope.configuration import xmlconfig
-from zope.interface.verify import verifyObject
import mailman.config.config
import mailman.core.logging
-from mailman.interfaces.database import IDatabase
+from mailman.interfaces.database import IDatabaseFactory
from mailman.utilities.modules import call_name
# The test infrastructure uses this to prevent the search and loading of any
@@ -110,6 +110,10 @@ def initialize_1(config_path=None):
# o-rwx although I think in most cases it doesn't hurt if other can read
# or write the files.
os.umask(007)
+ # Initialize configuration event subscribers. This must be done before
+ # setting up the configuration system.
+ from mailman.app.events import initialize as initialize_events
+ initialize_events()
# config_path will be set if the command line argument -C is given. That
# case overrides all others. When not given on the command line, the
# configuration file is searched for in the file system.
@@ -121,9 +125,10 @@ def initialize_1(config_path=None):
mailman.config.config.load(config_path)
-def initialize_2(debug=False, propagate_logs=None):
+def initialize_2(debug=False, propagate_logs=None, testing=False):
"""Second initialization step.
+ * Database
* Logging
* Pre-hook
* Rules
@@ -144,15 +149,11 @@ def initialize_2(debug=False, propagate_logs=None):
call_name(config.mailman.pre_hook)
# Instantiate the database class, ensure that it's of the right type, and
# initialize it. Then stash the object on our configuration object.
- database_class = config.database['class']
- database = call_name(database_class)
- verifyObject(IDatabase, database)
- database.initialize(debug)
- config.db = database
+ utility_name = ('testing' if testing else 'production')
+ config.db = getUtility(IDatabaseFactory, utility_name).create()
# Initialize the rules and chains. Do the imports here so as to avoid
# circular imports.
from mailman.app.commands import initialize as initialize_commands
- from mailman.app.events import initialize as initialize_events
from mailman.core.chains import initialize as initialize_chains
from mailman.core.pipelines import initialize as initialize_pipelines
from mailman.core.rules import initialize as initialize_rules
@@ -161,7 +162,6 @@ def initialize_2(debug=False, propagate_logs=None):
initialize_chains()
initialize_pipelines()
initialize_commands()
- initialize_events()
def initialize_3():
diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py
index 25bb68030..972417c2c 100644
--- a/src/mailman/core/pipelines.py
+++ b/src/mailman/core/pipelines.py
@@ -32,7 +32,7 @@ __all__ = [
import logging
-from zope.interface import implements
+from zope.interface import implementer
from zope.interface.verify import verifyObject
from mailman.app.bounces import bounce_message
@@ -75,11 +75,10 @@ def process(mlist, msg, msgdata, pipeline_name='built-in'):
+@implementer(IPipeline)
class BasePipeline:
"""Base pipeline implementation."""
- implements(IPipeline)
-
_default_handlers = ()
def __init__(self):
diff --git a/src/mailman/core/runner.py b/src/mailman/core/runner.py
index e86741c41..a79f19fbc 100644
--- a/src/mailman/core/runner.py
+++ b/src/mailman/core/runner.py
@@ -17,7 +17,7 @@
"""The process runner base class."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -32,14 +32,15 @@ import traceback
from cStringIO import StringIO
from lazr.config import as_boolean, as_timedelta
from zope.component import getUtility
-from zope.interface import implements
+from zope.event import notify
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
from mailman.core.switchboard import Switchboard
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager
-from mailman.interfaces.runner import IRunner
+from mailman.interfaces.runner import IRunner, RunnerCrashEvent
from mailman.utilities.string import expand
@@ -48,9 +49,8 @@ elog = logging.getLogger('mailman.error')
+@implementer(IRunner)
class Runner:
- implements(IRunner)
-
intercept_signals = True
def __init__(self, name, slice=None):
@@ -217,7 +217,12 @@ class Runner:
language = mlist.preferred_language
with _.using(language.code):
msgdata['lang'] = language.code
- keepqueued = self._dispose(mlist, msg, msgdata)
+ try:
+ keepqueued = self._dispose(mlist, msg, msgdata)
+ except Exception as error:
+ # Trigger the Zope event and re-raise
+ notify(RunnerCrashEvent(self, mlist, msg, msgdata, error))
+ raise
if keepqueued:
self.switchboard.enqueue(msg, msgdata)
diff --git a/src/mailman/core/switchboard.py b/src/mailman/core/switchboard.py
index 7cab4f4ad..1f16cb5fb 100644
--- a/src/mailman/core/switchboard.py
+++ b/src/mailman/core/switchboard.py
@@ -24,11 +24,12 @@ written. First, the message is written to the pickle, then the metadata
dictionary is written.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Switchboard',
+ 'handle_ConfigurationUpdatedEvent',
]
@@ -40,10 +41,11 @@ import cPickle
import hashlib
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.email.message import Message
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.switchboard import ISwitchboard
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
@@ -63,20 +65,9 @@ elog = logging.getLogger('mailman.error')
+@implementer(ISwitchboard)
class Switchboard:
- implements(ISwitchboard)
-
- @staticmethod
- def initialize():
- """Initialize the global switchboards for input/output."""
- for conf in config.runner_configs:
- name = conf.name.split('.')[-1]
- assert name not in config.switchboards, (
- 'Duplicate runner name: {0}'.format(name))
- substitutions = config.paths
- substitutions['name'] = name
- path = expand(conf.path, substitutions)
- config.switchboards[name] = Switchboard(name, path)
+ """See `ISwitchboard`."""
def __init__(self, name, queue_directory,
slice=None, numslices=1, recover=False):
@@ -263,3 +254,19 @@ class Switchboard:
self.finish(filebase, preserve=True)
else:
os.rename(src, dst)
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ """Initialize the global switchboards for input/output."""
+ if not isinstance(event, ConfigurationUpdatedEvent):
+ return
+ config = event.config
+ for conf in config.runner_configs:
+ name = conf.name.split('.')[-1]
+ assert name not in config.switchboards, (
+ 'Duplicate runner name: {0}'.format(name))
+ substitutions = config.paths
+ substitutions['name'] = name
+ path = expand(conf.path, substitutions)
+ config.switchboards[name] = Switchboard(name, path)
diff --git a/src/mailman/core/system.py b/src/mailman/core/system.py
index ce66761a7..b29567827 100644
--- a/src/mailman/core/system.py
+++ b/src/mailman/core/system.py
@@ -17,7 +17,7 @@
"""System information."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,15 +27,16 @@ __all__ = [
import sys
-from zope.interface import implements
+from zope.interface import implementer
from mailman import version
from mailman.interfaces.system import ISystem
+@implementer(ISystem)
class System:
- implements(ISystem)
+ """See `ISystem`."""
@property
def mailman_version(self):
diff --git a/src/mailman/core/tests/test_pipelines.py b/src/mailman/core/tests/test_pipelines.py
index 8f851de95..8e76cf033 100644
--- a/src/mailman/core/tests/test_pipelines.py
+++ b/src/mailman/core/tests/test_pipelines.py
@@ -29,7 +29,7 @@ __all__ = [
import unittest
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.lifecycle import create_list
from mailman.config import config
@@ -48,24 +48,24 @@ from mailman.testing.layers import ConfigLayer
+@implementer(IHandler)
class DiscardingHandler:
- implements(IHandler)
name = 'discarding'
def process(self, mlist, msg, msgdata):
raise DiscardMessage('by test handler')
+@implementer(IHandler)
class RejectHandler:
- implements(IHandler)
name = 'rejecting'
def process(self, mlist, msg, msgdata):
raise RejectMessage('by test handler')
+@implementer(IPipeline)
class DiscardingPipeline:
- implements(IPipeline)
name = 'test-discarding'
description = 'Discarding test pipeline'
@@ -73,8 +73,8 @@ class DiscardingPipeline:
yield DiscardingHandler()
+@implementer(IPipeline)
class RejectingPipeline:
- implements(IPipeline)
name = 'test-rejecting'
description = 'Rejectinging test pipeline'
diff --git a/src/mailman/core/tests/test_runner.py b/src/mailman/core/tests/test_runner.py
new file mode 100644
index 000000000..ad2548adc
--- /dev/null
+++ b/src/mailman/core/tests/test_runner.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test some Runner base class behavior."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestRunner',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.core.runner import Runner
+from mailman.interfaces.runner import RunnerCrashEvent
+from mailman.testing.helpers import (
+ configuration, event_subscribers, get_queue_messages,
+ make_testable_runner, specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class CrashingRunner(Runner):
+ def _dispose(self, mlist, msg, msgdata):
+ raise RuntimeError('borked')
+
+
+
+class TestRunner(unittest.TestCase):
+ """Test the Runner base class behavior."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._events = []
+
+ def _got_event(self, event):
+ self._events.append(event)
+
+ @configuration('runner.crashing',
+ **{'class': 'mailman.core.tests.CrashingRunner'})
+ def test_crash_event(self):
+ runner = make_testable_runner(CrashingRunner, 'in')
+ # When an exception occurs in Runner._process_one_file(), a zope.event
+ # gets triggered containing the exception object.
+ msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Message-ID: <ant>
+
+""")
+ config.switchboards['in'].enqueue(msg, listname='test@example.com')
+ with event_subscribers(self._got_event):
+ runner.run()
+ # We should now have exactly one event, which will contain the
+ # exception, plus additional metadata containing the mailing list,
+ # message, and metadata.
+ self.assertEqual(len(self._events), 1)
+ event = self._events[0]
+ self.assertTrue(isinstance(event, RunnerCrashEvent))
+ self.assertEqual(event.mailing_list, self._mlist)
+ self.assertEqual(event.message['message-id'], '<ant>')
+ self.assertEqual(event.metadata['listname'], 'test@example.com')
+ self.assertTrue(isinstance(event.error, RuntimeError))
+ self.assertEqual(event.error.message, 'borked')
+ self.assertTrue(isinstance(event.runner, CrashingRunner))
+ # The message should also have ended up in the shunt queue.
+ shunted = get_queue_messages('shunt')
+ self.assertEqual(len(shunted), 1)
+ self.assertEqual(shunted[0].msg['message-id'], '<ant>')
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index a69e99395..694ad9ec3 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -32,7 +32,7 @@ from lazr.config import as_boolean
from pkg_resources import resource_listdir, resource_string
from storm.cache import GenerationalCache
from storm.locals import create_database, Store
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.database import IDatabase
@@ -45,6 +45,7 @@ NL = '\n'
+@implementer(IDatabase)
class StormBaseDatabase:
"""The database base class for use with the Storm ORM.
@@ -55,8 +56,6 @@ class StormBaseDatabase:
# classes.
TAG = ''
- implements(IDatabase)
-
def __init__(self):
self.url = None
self.store = None
@@ -143,17 +142,21 @@ class StormBaseDatabase:
database.DEBUG = (as_boolean(config.database.debug)
if debug is None else debug)
self.store = store
- self.load_migrations()
store.commit()
- def load_migrations(self):
- """Load all not-yet loaded migrations."""
+ def load_migrations(self, until=None):
+ """Load schema migrations.
+
+ :param until: Load only the migrations up to the specified timestamp.
+ With default value of None, load all migrations.
+ :type until: string
+ """
migrations_path = config.database.migrations_path
if '.' in migrations_path:
parent, dot, child = migrations_path.rpartition('.')
else:
parent = migrations_path
- child =''
+ child = ''
# If the database does not yet exist, load the base schema.
filenames = sorted(resource_listdir(parent, child))
# Find out which schema migrations have already been loaded.
@@ -169,16 +172,40 @@ class StormBaseDatabase:
parts = module_fn.split('_')
if len(parts) < 2:
continue
- version = parts[1]
+ version = parts[1].strip()
+ if len(version) == 0:
+ # Not a schema migration file.
+ continue
if version in versions:
- # This one is already loaded.
+ log.debug('already migrated to %s', version)
continue
+ if until is not None and version > until:
+ # We're done.
+ break
module_path = migrations_path + '.' + module_fn
__import__(module_path)
upgrade = getattr(sys.modules[module_path], 'upgrade', None)
if upgrade is None:
continue
+ log.debug('migrating db to %s: %s', version, module_path)
upgrade(self, self.store, version, module_path)
+ self.commit()
+
+ def load_sql(self, store, sql):
+ """Load the given SQL into the store.
+
+ :param store: The Storm store to load the schema into.
+ :type store: storm.locals.Store`
+ :param sql: The possibly multi-line SQL to load.
+ :type sql: string
+ """
+ # Discard all blank and comment lines.
+ lines = (line for line in sql.splitlines()
+ if line.strip() != '' and line.strip()[:2] != '--')
+ sql = NL.join(lines)
+ for statement in sql.split(';'):
+ if statement.strip() != '':
+ store.execute(statement + ';')
def load_schema(self, store, version, filename, module_path):
"""Load the schema from a file.
@@ -187,7 +214,7 @@ class StormBaseDatabase:
:param store: The Storm store to load the schema into.
:type store: storm.locals.Store`
- :param version: The schema version identifier of the form
+ :param version: The schema version identifier of the form
YYYYMMDDHHMMSS.
:type version: string
:param filename: The file name containing the schema to load. Pass
@@ -200,22 +227,10 @@ class StormBaseDatabase:
"""
if filename is not None:
contents = resource_string('mailman.database.schema', filename)
- # Discard all blank and comment lines.
- lines = (line for line in contents.splitlines()
- if line.strip() != '' and line.strip()[:2] != '--')
- sql = NL.join(lines)
- for statement in sql.split(';'):
- if statement.strip() != '':
- store.execute(statement + ';')
+ self.load_sql(store, contents)
# Add a marker that indicates the migration version being applied.
store.add(Version(component='schema', version=version))
- # Add a marker so that the module name can be found later. This is
- # used by the test suite to reset the database between tests.
- store.add(Version(component=version, version=module_path))
- def _reset(self):
- """See `IDatabase`."""
- from mailman.database.model import ModelMeta
- self.store.rollback()
- ModelMeta._reset(self.store)
- self.store.commit()
+ @staticmethod
+ def _make_temporary():
+ raise NotImplementedError
diff --git a/src/mailman/database/docs/migration.rst b/src/mailman/database/docs/migration.rst
index 9897b1ef2..6216f9bbc 100644
--- a/src/mailman/database/docs/migration.rst
+++ b/src/mailman/database/docs/migration.rst
@@ -24,17 +24,18 @@ Migrations are applied automatically when Mailman starts up, but can also be
applied at any time by calling in the API directly. Once applied, a
migration's version string is registered so it will not be applied again.
-We see that the base migration is already applied.
+We see that the base migration, as well as subsequent standard migrations, are
+already applied.
>>> from mailman.model.version import Version
>>> results = config.db.store.find(Version, component='schema')
>>> results.count()
- 1
- >>> base = results.one()
- >>> print base.component
- schema
- >>> print base.version
+ 2
+ >>> versions = sorted(result.version for result in results)
+ >>> for version in versions:
+ ... print version
00000000000000
+ 20120407000000
Migrations
@@ -55,6 +56,14 @@ specified in the configuration file.
... migrations_path: migrations
... """)
+.. Clean this up at the end of the doctest.
+ >>> def cleanup():
+ ... import shutil
+ ... from mailman.config import config
+ ... config.pop('migrations')
+ ... shutil.rmtree(tempdir)
+ >>> cleanups.append(cleanup)
+
Here is an example migrations module. The key part of this interface is the
``upgrade()`` method, which takes four arguments:
@@ -69,7 +78,7 @@ This migration module just adds a marker to the `version` table.
>>> with open(os.path.join(path, '__init__.py'), 'w') as fp:
... pass
- >>> with open(os.path.join(path, 'mm_20120211000000.py'), 'w') as fp:
+ >>> with open(os.path.join(path, 'mm_20129999000000.py'), 'w') as fp:
... print >> fp, """
... from __future__ import unicode_literals
... from mailman.model.version import Version
@@ -86,14 +95,15 @@ This will load the new migration, since it hasn't been loaded before.
>>> for result in sorted(result.version for result in results):
... print result
00000000000000
- 20120211000000
+ 20120407000000
+ 20129999000000
>>> test = config.db.store.find(Version, component='test').one()
>>> print test.version
- 20120211000000
+ 20129999000000
Migrations will only be loaded once.
- >>> with open(os.path.join(path, 'mm_20120211000001.py'), 'w') as fp:
+ >>> with open(os.path.join(path, 'mm_20129999000001.py'), 'w') as fp:
... print >> fp, """
... from __future__ import unicode_literals
... from mailman.model.version import Version
@@ -115,13 +125,14 @@ The first time we load this new migration, we'll get the 801 marker.
>>> for result in sorted(result.version for result in results):
... print result
00000000000000
- 20120211000000
- 20120211000001
+ 20120407000000
+ 20129999000000
+ 20129999000001
>>> test = config.db.store.find(Version, component='test')
>>> for marker in sorted(marker.version for marker in test):
... print marker
00000000000801
- 20120211000000
+ 20129999000000
We do not get an 802 marker because the migration has already been loaded.
@@ -130,17 +141,56 @@ We do not get an 802 marker because the migration has already been loaded.
>>> for result in sorted(result.version for result in results):
... print result
00000000000000
- 20120211000000
- 20120211000001
+ 20120407000000
+ 20129999000000
+ 20129999000001
>>> test = config.db.store.find(Version, component='test')
>>> for marker in sorted(marker.version for marker in test):
... print marker
00000000000801
- 20120211000000
+ 20129999000000
+
+
+Partial upgrades
+================
+
+It's possible (mostly for testing purposes) to only do a partial upgrade, by
+providing a timestamp to `load_migrations()`. To demonstrate this, we add two
+additional migrations, intended to be applied in sequential order.
+
+ >>> from shutil import copyfile
+ >>> from mailman.testing.helpers import chdir
+ >>> with chdir(path):
+ ... copyfile('mm_20129999000000.py', 'mm_20129999000002.py')
+ ... copyfile('mm_20129999000000.py', 'mm_20129999000003.py')
+ ... copyfile('mm_20129999000000.py', 'mm_20129999000004.py')
+
+Now, only migrate to the ...03 timestamp.
+
+ >>> config.db.load_migrations('20129999000003')
+
+You'll notice that the ...04 version is not present.
+
+ >>> results = config.db.store.find(Version, component='schema')
+ >>> for result in sorted(result.version for result in results):
+ ... print result
+ 00000000000000
+ 20120407000000
+ 20129999000000
+ 20129999000001
+ 20129999000002
+ 20129999000003
+
-.. Clean up the temporary directory::
+.. cleanup:
+ Because the Version table holds schema migration data, it will not be
+ cleaned up by the standard test suite. This is generally not a problem
+ for SQLite since each test gets a new database file, but for PostgreSQL,
+ this will cause migration.rst to fail on subsequent runs. So let's just
+ clean up the database explicitly.
- >>> config.pop('migrations')
- >>> sys.path.remove(tempdir)
- >>> import shutil
- >>> shutil.rmtree(tempdir)
+ >>> results = config.db.store.execute("""
+ ... DELETE FROM version WHERE version.version >= '201299990000'
+ ... OR version.component = 'test';
+ ... """)
+ >>> config.db.commit()
diff --git a/src/mailman/database/factory.py b/src/mailman/database/factory.py
new file mode 100644
index 000000000..127c4aaeb
--- /dev/null
+++ b/src/mailman/database/factory.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Database factory."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'DatabaseFactory',
+ 'DatabaseTemporaryFactory',
+ 'DatabaseTestingFactory',
+ ]
+
+
+import types
+
+from zope.component import getAdapter
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
+
+from mailman.config import config
+from mailman.interfaces.database import (
+ IDatabase, IDatabaseFactory, ITemporaryDatabase)
+from mailman.utilities.modules import call_name
+
+
+
+@implementer(IDatabaseFactory)
+class DatabaseFactory:
+ """Create a new database."""
+
+ @staticmethod
+ def create():
+ """See `IDatabaseFactory`."""
+ database_class = config.database['class']
+ database = call_name(database_class)
+ verifyObject(IDatabase, database)
+ database.initialize()
+ database.load_migrations()
+ database.commit()
+ return database
+
+
+
+def _reset(self):
+ """See `IDatabase`."""
+ from mailman.database.model import ModelMeta
+ self.store.rollback()
+ self._pre_reset(self.store)
+ ModelMeta._reset(self.store)
+ self._post_reset(self.store)
+ self.store.commit()
+
+
+@implementer(IDatabaseFactory)
+class DatabaseTestingFactory:
+ """Create a new database for testing."""
+
+ @staticmethod
+ def create():
+ """See `IDatabaseFactory`."""
+ database_class = config.database['class']
+ database = call_name(database_class)
+ verifyObject(IDatabase, database)
+ database.initialize()
+ database.load_migrations()
+ database.commit()
+ # Make _reset() a bound method of the database instance.
+ database._reset = types.MethodType(_reset, database)
+ return database
+
+
+
+@implementer(IDatabaseFactory)
+class DatabaseTemporaryFactory:
+ """Create a temporary database for some of the migration tests."""
+
+ @staticmethod
+ def create():
+ """See `IDatabaseFactory`."""
+ database_class_name = config.database['class']
+ database = call_name(database_class_name)
+ verifyObject(IDatabase, database)
+ adapted_database = getAdapter(
+ database, ITemporaryDatabase, database.TAG)
+ return adapted_database
diff --git a/src/mailman/database/model.py b/src/mailman/database/model.py
index c45517c9b..58d5942a4 100644
--- a/src/mailman/database/model.py
+++ b/src/mailman/database/model.py
@@ -17,7 +17,7 @@
"""Base class for all database classes."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,8 +25,6 @@ __all__ = [
]
-import sys
-
from operator import attrgetter
from storm.properties import PropertyPublisherMeta
@@ -44,46 +42,24 @@ class ModelMeta(PropertyPublisherMeta):
self.__storm_table__ = name.lower()
super(ModelMeta, self).__init__(name, bases, dict)
# Register the model class so that it can be more easily cleared.
- # This is required by the test framework.
- if name == 'Model':
- return
- ModelMeta._class_registry.add(self)
+ # This is required by the test framework so that the corresponding
+ # table can be reset between tests.
+ #
+ # The PRESERVE flag indicates whether the table should be reset or
+ # not. We have to handle the actual Model base class explicitly
+ # because it does not correspond to a table in the database.
+ if not getattr(self, 'PRESERVE', False) and name != 'Model':
+ ModelMeta._class_registry.add(self)
@staticmethod
def _reset(store):
from mailman.config import config
- from mailman.model.version import Version
config.db._pre_reset(store)
- # Give each schema migration a chance to do its pre-reset. See below
- # for calling its post reset too.
- versions = sorted(version.version for version in
- store.find(Version, component='schema'))
- migrations = {}
- for version in versions:
- # We have to give the migrations module that loaded this version a
- # chance to do both pre- and post-reset operations. The following
- # find the actual the module path for the migration. See
- # StormBaseDatabase.load_schema().
- migration = store.find(Version, component=version).one()
- if migration is None:
- continue
- migrations[version] = module_path = migration.version
- module = sys.modules[module_path]
- pre_reset = getattr(module, 'pre_reset', None)
- if pre_reset is not None:
- pre_reset(store)
# Make sure this is deterministic, by sorting on the storm table name.
classes = sorted(ModelMeta._class_registry,
key=attrgetter('__storm_table__'))
for model_class in classes:
store.find(model_class).remove()
- # Now give each migration a chance to do post-reset operations.
- for version in versions:
- module = sys.modules[migrations[version]]
- post_reset = getattr(module, 'post_reset', None)
- if post_reset is not None:
- post_reset(store)
- config.db._post_reset(store)
diff --git a/src/mailman/database/postgresql.py b/src/mailman/database/postgresql.py
index 988f7a1af..49188148f 100644
--- a/src/mailman/database/postgresql.py
+++ b/src/mailman/database/postgresql.py
@@ -17,17 +17,23 @@
"""PostgreSQL database support."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'PostgreSQLDatabase',
+ 'make_temporary',
]
+import types
+
+from functools import partial
from operator import attrgetter
+from urlparse import urlsplit, urlunsplit
from mailman.database.base import StormBaseDatabase
+from mailman.testing.helpers import configuration
@@ -40,8 +46,8 @@ class PostgreSQLDatabase(StormBaseDatabase):
"""See `BaseDatabase`."""
table_query = ('SELECT table_name FROM information_schema.tables '
"WHERE table_schema = 'public'")
- table_names = set(item[0] for item in
- store.execute(table_query))
+ results = store.execute(table_query)
+ table_names = set(item[0] for item in results)
return 'version' in table_names
def _post_reset(self, store):
@@ -63,3 +69,37 @@ class PostgreSQLDatabase(StormBaseDatabase):
max("id") IS NOT null)
FROM "{0}";
""".format(model_class.__storm_table__))
+
+
+
+# Test suite adapter for ITemporaryDatabase.
+
+def _cleanup(self, store, tempdb_name):
+ from mailman.config import config
+ store.rollback()
+ store.close()
+ # From the original database connection, drop the now unused database.
+ config.db.store.execute('DROP DATABASE {0}'.format(tempdb_name))
+
+
+def make_temporary(database):
+ """Adapts by monkey patching an existing PostgreSQL IDatabase."""
+ from mailman.config import config
+ parts = urlsplit(config.database.url)
+ assert parts.scheme == 'postgres'
+ new_parts = list(parts)
+ new_parts[2] = '/mmtest'
+ url = urlunsplit(new_parts)
+ # Use the existing database connection to create a new testing
+ # database.
+ config.db.store.execute('ABORT;')
+ config.db.store.execute('CREATE DATABASE mmtest;')
+ with configuration('database', url=url):
+ database.initialize()
+ database._cleanup = types.MethodType(
+ partial(_cleanup, store=database.store, tempdb_name='mmtest'),
+ database)
+ # bool column values in PostgreSQL.
+ database.FALSE = 'False'
+ database.TRUE = 'True'
+ return database
diff --git a/src/mailman/database/schema/mm_00000000000000_base.py b/src/mailman/database/schema/mm_00000000000000_base.py
index d703088d6..0dcd28edd 100644
--- a/src/mailman/database/schema/mm_00000000000000_base.py
+++ b/src/mailman/database/schema/mm_00000000000000_base.py
@@ -22,34 +22,14 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'upgrade',
- 'post_reset',
- 'pre_reset',
]
-_migration_path = None
VERSION = '00000000000000'
+_helper = None
def upgrade(database, store, version, module_path):
filename = '{0}.sql'.format(database.TAG)
database.load_schema(store, version, filename, module_path)
-
-
-def pre_reset(store):
- global _migration_path
- # Save the entry in the Version table for the test suite reset. This will
- # be restored below.
- from mailman.model.version import Version
- result = store.find(Version, component=VERSION).one()
- # Yes, we abuse this field.
- _migration_path = result.version
-
-
-def post_reset(store):
- from mailman.model.version import Version
- # We need to preserve the Version table entry for this migration, since
- # its existence defines the fact that the tables have been loaded.
- store.add(Version(component='schema', version=VERSION))
- store.add(Version(component=VERSION, version=_migration_path))
diff --git a/src/mailman/database/schema/mm_20120407000000.py b/src/mailman/database/schema/mm_20120407000000.py
new file mode 100644
index 000000000..d6e647c1a
--- /dev/null
+++ b/src/mailman/database/schema/mm_20120407000000.py
@@ -0,0 +1,214 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""3.0b1 -> 3.0b2 schema migrations.
+
+All column changes are in the `mailinglist` table.
+
+* Renames:
+ - news_prefix_subject_too -> nntp_prefix_subject_too
+ - news_moderation -> newsgroup_moderation
+
+* Collapsing:
+ - archive, archive_private -> archive_policy
+
+* Remove:
+ - archive_volume_frequency
+ - generic_nonmember_action
+ - nntp_host
+
+* Added:
+ - list_id
+
+* Changes:
+ member.mailing_list holds the list_id not the fqdn_listname
+
+See https://bugs.launchpad.net/mailman/+bug/971013 for details.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'upgrade',
+ ]
+
+
+from mailman.interfaces.archiver import ArchivePolicy
+
+
+VERSION = '20120407000000'
+_helper = None
+
+
+
+def upgrade(database, store, version, module_path):
+ if database.TAG == 'sqlite':
+ upgrade_sqlite(database, store, version, module_path)
+ else:
+ upgrade_postgres(database, store, version, module_path)
+
+
+
+def archive_policy(archive, archive_private):
+ """Convert archive and archive_private to archive_policy."""
+ if archive == 0:
+ return int(ArchivePolicy.never)
+ elif archive_private == 1:
+ return int(ArchivePolicy.private)
+ else:
+ return int(ArchivePolicy.public)
+
+
+
+def upgrade_sqlite(database, store, version, module_path):
+ # Load the first part of the migration. This creates a temporary table to
+ # hold the new mailinglist table columns. The problem is that some of the
+ # changes must be performed in Python, so after the first part is loaded,
+ # we do the Python changes, drop the old mailing list table, and then
+ # rename the temporary table to its place.
+ database.load_schema(
+ store, version, 'sqlite_{0}_01.sql'.format(version), module_path)
+ results = store.execute("""
+ SELECT id, include_list_post_header,
+ news_prefix_subject_too, news_moderation,
+ archive, archive_private, list_name, mail_host
+ FROM mailinglist;
+ """)
+ for value in results:
+ (id, list_post,
+ news_prefix, news_moderation,
+ archive, archive_private,
+ list_name, mail_host) = value
+ # Figure out what the new archive_policy column value should be.
+ list_id = '{0}.{1}'.format(list_name, mail_host)
+ fqdn_listname = '{0}@{1}'.format(list_name, mail_host)
+ store.execute("""
+ UPDATE ml_backup SET
+ allow_list_posts = {0},
+ newsgroup_moderation = {1},
+ nntp_prefix_subject_too = {2},
+ archive_policy = {3},
+ list_id = '{4}'
+ WHERE id = {5};
+ """.format(
+ list_post,
+ news_moderation,
+ news_prefix,
+ archive_policy(archive, archive_private),
+ list_id,
+ id))
+ # Also update the member.mailing_list column to hold the list_id
+ # instead of the fqdn_listname.
+ store.execute("""
+ UPDATE member SET
+ mailing_list = '{0}'
+ WHERE mailing_list = '{1}';
+ """.format(list_id, fqdn_listname))
+ # Pivot the backup table to the real thing.
+ store.execute('DROP TABLE mailinglist;')
+ store.execute('ALTER TABLE ml_backup RENAME TO mailinglist;')
+ # Now add some indexes that were previously missing.
+ store.execute(
+ 'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);')
+ store.execute(
+ 'CREATE INDEX ix_mailinglist_fqdn_listname '
+ 'ON mailinglist (list_name, mail_host);')
+ # Now, do the member table.
+ results = store.execute('SELECT id, mailing_list FROM member;')
+ for id, mailing_list in results:
+ list_name, at, mail_host = mailing_list.partition('@')
+ if at == '':
+ list_id = mailing_list
+ else:
+ list_id = '{0}.{1}'.format(list_name, mail_host)
+ store.execute("""
+ UPDATE mem_backup SET list_id = '{0}'
+ WHERE id = {1};
+ """.format(list_id, id))
+ # Pivot the backup table to the real thing.
+ store.execute('DROP TABLE member;')
+ store.execute('ALTER TABLE mem_backup RENAME TO member;')
+
+
+
+def upgrade_postgres(database, store, version, module_path):
+ # Get the old values from the mailinglist table.
+ results = store.execute("""
+ SELECT id, archive, archive_private, list_name, mail_host
+ FROM mailinglist;
+ """)
+ # Do the simple renames first.
+ store.execute("""
+ ALTER TABLE mailinglist
+ RENAME COLUMN news_prefix_subject_too TO nntp_prefix_subject_too;
+ """)
+ store.execute("""
+ ALTER TABLE mailinglist
+ RENAME COLUMN news_moderation TO newsgroup_moderation;
+ """)
+ store.execute("""
+ ALTER TABLE mailinglist
+ RENAME COLUMN include_list_post_header TO allow_list_posts;
+ """)
+ # Do the easy column drops next.
+ for column in ('archive_volume_frequency',
+ 'generic_nonmember_action',
+ 'nntp_host'):
+ store.execute(
+ 'ALTER TABLE mailinglist DROP COLUMN {0};'.format(column))
+ # Now do the trickier collapsing of values. Add the new columns.
+ store.execute('ALTER TABLE mailinglist ADD COLUMN archive_policy INTEGER;')
+ store.execute('ALTER TABLE mailinglist ADD COLUMN list_id TEXT;')
+ # Query the database for the old values of archive and archive_private in
+ # each column. Then loop through all the results and update the new
+ # archive_policy from the old values.
+ for value in results:
+ id, archive, archive_private, list_name, mail_host = value
+ list_id = '{0}.{1}'.format(list_name, mail_host)
+ store.execute("""
+ UPDATE mailinglist SET
+ archive_policy = {0},
+ list_id = '{1}'
+ WHERE id = {2};
+ """.format(archive_policy(archive, archive_private), list_id, id))
+ # Now drop the old columns.
+ for column in ('archive', 'archive_private'):
+ store.execute(
+ 'ALTER TABLE mailinglist DROP COLUMN {0};'.format(column))
+ # Now add some indexes that were previously missing.
+ store.execute(
+ 'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);')
+ store.execute(
+ 'CREATE INDEX ix_mailinglist_fqdn_listname '
+ 'ON mailinglist (list_name, mail_host);')
+ # Now, do the member table.
+ results = store.execute('SELECT id, mailing_list FROM member;')
+ store.execute('ALTER TABLE member ADD COLUMN list_id TEXT;')
+ for id, mailing_list in results:
+ list_name, at, mail_host = mailing_list.partition('@')
+ if at == '':
+ list_id = mailing_list
+ else:
+ list_id = '{0}.{1}'.format(list_name, mail_host)
+ store.execute("""
+ UPDATE member SET list_id = '{0}'
+ WHERE id = {1};
+ """.format(list_id, id))
+ store.execute('ALTER TABLE member DROP COLUMN mailing_list;')
+ # Record the migration in the version table.
+ database.load_schema(store, version, None, module_path)
diff --git a/src/mailman/database/schema/postgres.sql b/src/mailman/database/schema/postgres.sql
index 2e9ba249f..0e97a4332 100644
--- a/src/mailman/database/schema/postgres.sql
+++ b/src/mailman/database/schema/postgres.sql
@@ -110,7 +110,8 @@ CREATE TABLE mailinglist (
topics_enabled BOOLEAN,
unsubscribe_policy INTEGER,
welcome_message_uri TEXT,
- moderation_callback TEXT,
+ -- This was accidentally added by the PostgreSQL porter.
+ -- moderation_callback TEXT,
PRIMARY KEY (id)
);
diff --git a/src/mailman/database/schema/sqlite.sql b/src/mailman/database/schema/sqlite.sql
index e6211bf53..e2b2d3814 100644
--- a/src/mailman/database/schema/sqlite.sql
+++ b/src/mailman/database/schema/sqlite.sql
@@ -1,3 +1,6 @@
+-- THIS FILE HAS BEEN FROZEN AS OF 3.0b1
+-- SEE THE SCHEMA MIGRATIONS FOR DIFFERENCES.
+
PRAGMA foreign_keys = ON;
CREATE TABLE _request (
diff --git a/src/mailman/database/schema/sqlite_20120407000000_01.sql b/src/mailman/database/schema/sqlite_20120407000000_01.sql
new file mode 100644
index 000000000..b93e214c4
--- /dev/null
+++ b/src/mailman/database/schema/sqlite_20120407000000_01.sql
@@ -0,0 +1,280 @@
+-- THIS FILE CONTAINS THE SQLITE3 SCHEMA MIGRATION FROM
+-- 3.0b1 TO 3.0b2
+--
+-- AFTER 3.0b2 IS RELEASED YOU MAY NOT EDIT THIS FILE.
+
+-- For SQLite3 migration strategy, see
+-- http://sqlite.org/faq.html#q11
+
+-- REMOVALS from the mailinglist table.
+-- REM archive
+-- REM archive_private
+-- REM archive_volume_frequency
+-- REM include_list_post_header
+-- REM news_moderation
+-- REM news_prefix_subject_too
+-- REM nntp_host
+--
+-- ADDS to the mailing list table.
+-- ADD allow_list_posts
+-- ADD archive_policy
+-- ADD list_id
+-- ADD newsgroup_moderation
+-- ADD nntp_prefix_subject_too
+
+-- LP: #971013
+-- LP: #967238
+
+-- REMOVALS from the member table.
+-- REM mailing_list
+
+-- ADDS to the member table.
+-- ADD list_id
+
+-- LP: #1024509
+
+
+CREATE TABLE ml_backup(
+ id INTEGER NOT NULL,
+ -- List identity
+ list_name TEXT,
+ mail_host TEXT,
+ allow_list_posts BOOLEAN,
+ include_rfc2369_headers BOOLEAN,
+ -- Attributes not directly modifiable via the web u/i
+ created_at TIMESTAMP,
+ admin_member_chunksize INTEGER,
+ next_request_id INTEGER,
+ next_digest_number INTEGER,
+ digest_last_sent_at TIMESTAMP,
+ volume INTEGER,
+ last_post_at TIMESTAMP,
+ accept_these_nonmembers BLOB,
+ acceptable_aliases_id INTEGER,
+ admin_immed_notify BOOLEAN,
+ admin_notify_mchanges BOOLEAN,
+ administrivia BOOLEAN,
+ advertised BOOLEAN,
+ anonymous_list BOOLEAN,
+ -- Automatic responses.
+ autorespond_owner INTEGER,
+ autoresponse_owner_text TEXT,
+ autorespond_postings INTEGER,
+ autoresponse_postings_text TEXT,
+ autorespond_requests INTEGER,
+ autoresponse_request_text TEXT,
+ autoresponse_grace_period TEXT,
+ -- Bounces.
+ forward_unrecognized_bounces_to INTEGER,
+ process_bounces BOOLEAN,
+ bounce_info_stale_after TEXT,
+ bounce_matching_headers TEXT,
+ bounce_notify_owner_on_disable BOOLEAN,
+ bounce_notify_owner_on_removal BOOLEAN,
+ bounce_score_threshold INTEGER,
+ bounce_you_are_disabled_warnings INTEGER,
+ bounce_you_are_disabled_warnings_interval TEXT,
+ -- Content filtering.
+ filter_action INTEGER,
+ filter_content BOOLEAN,
+ collapse_alternatives BOOLEAN,
+ convert_html_to_plaintext BOOLEAN,
+ default_member_action INTEGER,
+ default_nonmember_action INTEGER,
+ description TEXT,
+ digest_footer_uri TEXT,
+ digest_header_uri TEXT,
+ digest_is_default BOOLEAN,
+ digest_send_periodic BOOLEAN,
+ digest_size_threshold FLOAT,
+ digest_volume_frequency INTEGER,
+ digestable BOOLEAN,
+ discard_these_nonmembers BLOB,
+ emergency BOOLEAN,
+ encode_ascii_prefixes BOOLEAN,
+ first_strip_reply_to BOOLEAN,
+ footer_uri TEXT,
+ forward_auto_discards BOOLEAN,
+ gateway_to_mail BOOLEAN,
+ gateway_to_news BOOLEAN,
+ goodbye_message_uri TEXT,
+ header_matches BLOB,
+ header_uri TEXT,
+ hold_these_nonmembers BLOB,
+ info TEXT,
+ linked_newsgroup TEXT,
+ max_days_to_hold INTEGER,
+ max_message_size INTEGER,
+ max_num_recipients INTEGER,
+ member_moderation_notice TEXT,
+ mime_is_default_digest BOOLEAN,
+ moderator_password TEXT,
+ new_member_options INTEGER,
+ nondigestable BOOLEAN,
+ nonmember_rejection_notice TEXT,
+ obscure_addresses BOOLEAN,
+ owner_chain TEXT,
+ owner_pipeline TEXT,
+ personalize INTEGER,
+ post_id INTEGER,
+ posting_chain TEXT,
+ posting_pipeline TEXT,
+ preferred_language TEXT,
+ private_roster BOOLEAN,
+ display_name TEXT,
+ reject_these_nonmembers BLOB,
+ reply_goes_to_list INTEGER,
+ reply_to_address TEXT,
+ require_explicit_destination BOOLEAN,
+ respond_to_post_requests BOOLEAN,
+ scrub_nondigest BOOLEAN,
+ send_goodbye_message BOOLEAN,
+ send_reminders BOOLEAN,
+ send_welcome_message BOOLEAN,
+ subject_prefix TEXT,
+ subscribe_auto_approval BLOB,
+ subscribe_policy INTEGER,
+ topics BLOB,
+ topics_bodylines_limit INTEGER,
+ topics_enabled BOOLEAN,
+ unsubscribe_policy INTEGER,
+ welcome_message_uri TEXT,
+ PRIMARY KEY (id)
+ );
+
+INSERT INTO ml_backup SELECT
+ id,
+ -- List identity
+ list_name,
+ mail_host,
+ include_list_post_header,
+ include_rfc2369_headers,
+ -- Attributes not directly modifiable via the web u/i
+ created_at,
+ admin_member_chunksize,
+ next_request_id,
+ next_digest_number,
+ digest_last_sent_at,
+ volume,
+ last_post_at,
+ accept_these_nonmembers,
+ acceptable_aliases_id,
+ admin_immed_notify,
+ admin_notify_mchanges,
+ administrivia,
+ advertised,
+ anonymous_list,
+ -- Automatic responses.
+ autorespond_owner,
+ autoresponse_owner_text,
+ autorespond_postings,
+ autoresponse_postings_text,
+ autorespond_requests,
+ autoresponse_request_text,
+ autoresponse_grace_period,
+ -- Bounces.
+ forward_unrecognized_bounces_to,
+ process_bounces,
+ bounce_info_stale_after,
+ bounce_matching_headers,
+ bounce_notify_owner_on_disable,
+ bounce_notify_owner_on_removal,
+ bounce_score_threshold,
+ bounce_you_are_disabled_warnings,
+ bounce_you_are_disabled_warnings_interval,
+ -- Content filtering.
+ filter_action,
+ filter_content,
+ collapse_alternatives,
+ convert_html_to_plaintext,
+ default_member_action,
+ default_nonmember_action,
+ description,
+ digest_footer_uri,
+ digest_header_uri,
+ digest_is_default,
+ digest_send_periodic,
+ digest_size_threshold,
+ digest_volume_frequency,
+ digestable,
+ discard_these_nonmembers,
+ emergency,
+ encode_ascii_prefixes,
+ first_strip_reply_to,
+ footer_uri,
+ forward_auto_discards,
+ gateway_to_mail,
+ gateway_to_news,
+ goodbye_message_uri,
+ header_matches,
+ header_uri,
+ hold_these_nonmembers,
+ info,
+ linked_newsgroup,
+ max_days_to_hold,
+ max_message_size,
+ max_num_recipients,
+ member_moderation_notice,
+ mime_is_default_digest,
+ moderator_password,
+ new_member_options,
+ nondigestable,
+ nonmember_rejection_notice,
+ obscure_addresses,
+ owner_chain,
+ owner_pipeline,
+ personalize,
+ post_id,
+ posting_chain,
+ posting_pipeline,
+ preferred_language,
+ private_roster,
+ display_name,
+ reject_these_nonmembers,
+ reply_goes_to_list,
+ reply_to_address,
+ require_explicit_destination,
+ respond_to_post_requests,
+ scrub_nondigest,
+ send_goodbye_message,
+ send_reminders,
+ send_welcome_message,
+ subject_prefix,
+ subscribe_auto_approval,
+ subscribe_policy,
+ topics,
+ topics_bodylines_limit,
+ topics_enabled,
+ unsubscribe_policy,
+ welcome_message_uri
+ FROM mailinglist;
+
+CREATE TABLE mem_backup(
+ id INTEGER NOT NULL,
+ _member_id TEXT,
+ role INTEGER,
+ moderation_action INTEGER,
+ address_id INTEGER,
+ preferences_id INTEGER,
+ user_id INTEGER,
+ PRIMARY KEY (id)
+ );
+
+INSERT INTO mem_backup SELECT
+ id,
+ _member_id,
+ role,
+ moderation_action,
+ address_id,
+ preferences_id,
+ user_id
+ FROM member;
+
+
+-- Add the new columns. They'll get inserted at the Python layer.
+ALTER TABLE ml_backup ADD COLUMN archive_policy INTEGER;
+ALTER TABLE ml_backup ADD COLUMN list_id TEXT;
+ALTER TABLE ml_backup ADD COLUMN nntp_prefix_subject_too INTEGER;
+ALTER TABLE ml_backup ADD COLUMN newsgroup_moderation INTEGER;
+
+ALTER TABLE mem_backup ADD COLUMN list_id TEXT;
diff --git a/src/mailman/database/sqlite.py b/src/mailman/database/sqlite.py
index 2677d0d71..8415aa1ee 100644
--- a/src/mailman/database/sqlite.py
+++ b/src/mailman/database/sqlite.py
@@ -17,19 +17,25 @@
"""SQLite database support."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'SQLiteDatabase',
+ 'make_temporary',
]
import os
+import types
+import shutil
+import tempfile
+from functools import partial
from urlparse import urlparse
from mailman.database.base import StormBaseDatabase
+from mailman.testing.helpers import configuration
@@ -41,7 +47,7 @@ class SQLiteDatabase(StormBaseDatabase):
def _database_exists(self, store):
"""See `BaseDatabase`."""
table_query = 'select tbl_name from sqlite_master;'
- table_names = set(item[0] for item in
+ table_names = set(item[0] for item in
store.execute(table_query))
return 'version' in table_names
@@ -54,3 +60,25 @@ class SQLiteDatabase(StormBaseDatabase):
# Ignore errors
if fd > 0:
os.close(fd)
+
+
+
+# Test suite adapter for ITemporaryDatabase.
+
+def _cleanup(self, tempdir):
+ shutil.rmtree(tempdir)
+
+
+def make_temporary(database):
+ """Adapts by monkey patching an existing SQLite IDatabase."""
+ tempdir = tempfile.mkdtemp()
+ url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
+ with configuration('database', url=url):
+ database.initialize()
+ database._cleanup = types.MethodType(
+ partial(_cleanup, tempdir=tempdir),
+ database)
+ # bool column values in SQLite must be integers.
+ database.FALSE = 0
+ database.TRUE = 1
+ return database
diff --git a/src/mailman/database/tests/__init__.py b/src/mailman/database/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/database/tests/__init__.py
diff --git a/src/mailman/database/tests/data/__init__.py b/src/mailman/database/tests/data/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/database/tests/data/__init__.py
diff --git a/src/mailman/database/tests/data/mailman_01.db b/src/mailman/database/tests/data/mailman_01.db
new file mode 100644
index 000000000..1ff8d8343
--- /dev/null
+++ b/src/mailman/database/tests/data/mailman_01.db
Binary files differ
diff --git a/src/mailman/database/tests/data/migration_postgres_1.sql b/src/mailman/database/tests/data/migration_postgres_1.sql
new file mode 100644
index 000000000..b82ecf6e4
--- /dev/null
+++ b/src/mailman/database/tests/data/migration_postgres_1.sql
@@ -0,0 +1,133 @@
+INSERT INTO "acceptablealias" VALUES(1,'foo@example.com',1);
+INSERT INTO "acceptablealias" VALUES(2,'bar@example.com',1);
+
+INSERT INTO "address" VALUES(
+ 1,'anne@example.com',NULL,'Anne Person',
+ '2012-04-19 00:52:24.826432','2012-04-19 00:49:42.373769',1,2);
+INSERT INTO "address" VALUES(
+ 2,'bart@example.com',NULL,'Bart Person',
+ '2012-04-19 00:53:25.878800','2012-04-19 00:49:52.882050',2,4);
+
+INSERT INTO "domain" VALUES(
+ 1,'example.com','http://example.com',NULL,'postmaster@example.com');
+
+INSERT INTO "mailinglist" VALUES(
+ -- id,list_name,mail_host,include_list_post_header,include_rfc2369_headers
+ 1,'test','example.com',True,True,
+ -- created_at,admin_member_chunksize,next_request_id,next_digest_number
+ '2012-04-19 00:46:13.173844',30,1,1,
+ -- digest_last_sent_at,volume,last_post_at,accept_these_nonmembers
+ NULL,1,NULL,E'\\x80025D71012E',
+ -- acceptable_aliases_id,admin_immed_notify,admin_notify_mchanges
+ NULL,True,False,
+ -- administrivia,advertised,anonymous_list,archive,archive_private
+ True,True,False,True,False,
+ -- archive_volume_frequency
+ 1,
+ --autorespond_owner,autoresponse_owner_text
+ 0,'',
+ -- autorespond_postings,autoresponse_postings_text
+ 0,'',
+ -- autorespond_requests,authoresponse_requests_text
+ 0,'',
+ -- autoresponse_grace_period
+ '90 days, 0:00:00',
+ -- forward_unrecognized_bounces_to,process_bounces
+ 1,True,
+ -- bounce_info_stale_after,bounce_matching_headers
+ '7 days, 0:00:00','
+# Lines that *start* with a ''#'' are comments.
+to: friend@public.com
+message-id: relay.comanche.denmark.eu
+from: list@listme.com
+from: .*@uplinkpro.com
+',
+ -- bounce_notify_owner_on_disable,bounce_notify_owner_on_removal
+ True,True,
+ -- bounce_score_threshold,bounce_you_are_disabled_warnings
+ 5,3,
+ -- bounce_you_are_disabled_warnings_interval
+ '7 days, 0:00:00',
+ -- filter_action,filter_content,collapse_alternatives
+ 2,False,True,
+ -- convert_html_to_plaintext,default_member_action,default_nonmember_action
+ False,4,0,
+ -- description
+ '',
+ -- digest_footer_uri
+ 'mailman:///$listname/$language/footer-generic.txt',
+ -- digest_header_uri
+ NULL,
+ -- digest_is_default,digest_send_periodic,digest_size_threshold
+ False,True,30.0,
+ -- digest_volume_frequency,digestable,discard_these_nonmembers
+ 1,True,E'\\x80025D71012E',
+ -- emergency,encode_ascii_prefixes,first_strip_reply_to
+ False,False,False,
+ -- footer_uri
+ 'mailman:///$listname/$language/footer-generic.txt',
+ -- forward_auto_discards,gateway_to_mail,gateway_to_news
+ True,False,FAlse,
+ -- generic_nonmember_action,goodby_message_uri
+ 1,'',
+ -- header_matches,header_uri,hold_these_nonmembers,info,linked_newsgroup
+ E'\\x80025D71012E',NULL,E'\\x80025D71012E','','',
+ -- max_days_to_hold,max_message_size,max_num_recipients
+ 0,40,10,
+ -- member_moderation_notice,mime_is_default_digest,moderator_password
+ '',False,NULL,
+ -- new_member_options,news_moderation,news_prefix_subject_too
+ 256,0,True,
+ -- nntp_host,nondigestable,nonmember_rejection_notice,obscure_addresses
+ '',True,'',True,
+ -- owner_chain,owner_pipeline,personalize,post_id
+ 'default-owner-chain','default-owner-pipeline',0,1,
+ -- posting_chain,posting_pipeline,preferred_language,private_roster
+ 'default-posting-chain','default-posting-pipeline','en',True,
+ -- display_name,reject_these_nonmembers
+ 'Test',E'\\x80025D71012E',
+ -- reply_goes_to_list,reply_to_address
+ 0,'',
+ -- require_explicit_destination,respond_to_post_requests
+ True,True,
+ -- scrub_nondigest,send_goodbye_message,send_reminders,send_welcome_message
+ False,True,True,True,
+ -- subject_prefix,subscribe_auto_approval
+ '[Test] ',E'\\x80025D71012E',
+ -- subscribe_policy,topics,topics_bodylines_limit,topics_enabled
+ 1,E'\\x80025D71012E',5,False,
+ -- unsubscribe_policy,welcome_message_uri
+ 0,'mailman:///welcome.txt');
+
+INSERT INTO "member" VALUES(
+ 1,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1',1,'test@example.com',4,NULL,5,1);
+INSERT INTO "member" VALUES(
+ 2,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd',2,'test@example.com',3,NULL,6,1);
+INSERT INTO "member" VALUES(
+ 3,'479be431-45f2-473d-bc3c-7eac614030ac',3,'test@example.com',3,NULL,7,2);
+INSERT INTO "member" VALUES(
+ 4,'e2dc604c-d93a-4b91-b5a8-749e3caade36',1,'test@example.com',4,NULL,8,2);
+
+INSERT INTO "preferences" VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(3,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(4,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(5,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(6,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(7,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(8,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+
+INSERT INTO "user" VALUES(
+ 1,'Anne Person',NULL,'0adf3caa-6f26-46f8-a11d-5256c8148592',
+ '2012-04-19 00:49:42.370493',1,1);
+INSERT INTO "user" VALUES(
+ 2,'Bart Person',NULL,'63f5d1a2-e533-4055-afe4-475dec3b1163',
+ '2012-04-19 00:49:52.868746',2,3);
+
+INSERT INTO "uid" VALUES(1,'8bf9a615-f23e-4980-b7d1-90ac0203c66f');
+INSERT INTO "uid" VALUES(2,'0adf3caa-6f26-46f8-a11d-5256c8148592');
+INSERT INTO "uid" VALUES(3,'63f5d1a2-e533-4055-afe4-475dec3b1163');
+INSERT INTO "uid" VALUES(4,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1');
+INSERT INTO "uid" VALUES(5,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd');
+INSERT INTO "uid" VALUES(6,'479be431-45f2-473d-bc3c-7eac614030ac');
+INSERT INTO "uid" VALUES(7,'e2dc604c-d93a-4b91-b5a8-749e3caade36');
diff --git a/src/mailman/database/tests/data/migration_sqlite_1.sql b/src/mailman/database/tests/data/migration_sqlite_1.sql
new file mode 100644
index 000000000..a5ac96dfa
--- /dev/null
+++ b/src/mailman/database/tests/data/migration_sqlite_1.sql
@@ -0,0 +1,133 @@
+INSERT INTO "acceptablealias" VALUES(1,'foo@example.com',1);
+INSERT INTO "acceptablealias" VALUES(2,'bar@example.com',1);
+
+INSERT INTO "address" VALUES(
+ 1,'anne@example.com',NULL,'Anne Person',
+ '2012-04-19 00:52:24.826432','2012-04-19 00:49:42.373769',1,2);
+INSERT INTO "address" VALUES(
+ 2,'bart@example.com',NULL,'Bart Person',
+ '2012-04-19 00:53:25.878800','2012-04-19 00:49:52.882050',2,4);
+
+INSERT INTO "domain" VALUES(
+ 1,'example.com','http://example.com',NULL,'postmaster@example.com');
+
+INSERT INTO "mailinglist" VALUES(
+ -- id,list_name,mail_host,include_list_post_header,include_rfc2369_headers
+ 1,'test','example.com',1,1,
+ -- created_at,admin_member_chunksize,next_request_id,next_digest_number
+ '2012-04-19 00:46:13.173844',30,1,1,
+ -- digest_last_sent_at,volume,last_post_at,accept_these_nonmembers
+ NULL,1,NULL,X'80025D71012E',
+ -- acceptable_aliases_id,admin_immed_notify,admin_notify_mchanges
+ NULL,1,0,
+ -- administrivia,advertised,anonymous_list,archive,archive_private
+ 1,1,0,1,0,
+ -- archive_volume_frequency
+ 1,
+ --autorespond_owner,autoresponse_owner_text
+ 0,'',
+ -- autorespond_postings,autoresponse_postings_text
+ 0,'',
+ -- autorespond_requests,authoresponse_requests_text
+ 0,'',
+ -- autoresponse_grace_period
+ '90 days, 0:00:00',
+ -- forward_unrecognized_bounces_to,process_bounces
+ 1,1,
+ -- bounce_info_stale_after,bounce_matching_headers
+ '7 days, 0:00:00','
+# Lines that *start* with a ''#'' are comments.
+to: friend@public.com
+message-id: relay.comanche.denmark.eu
+from: list@listme.com
+from: .*@uplinkpro.com
+',
+ -- bounce_notify_owner_on_disable,bounce_notify_owner_on_removal
+ 1,1,
+ -- bounce_score_threshold,bounce_you_are_disabled_warnings
+ 5,3,
+ -- bounce_you_are_disabled_warnings_interval
+ '7 days, 0:00:00',
+ -- filter_action,filter_content,collapse_alternatives
+ 2,0,1,
+ -- convert_html_to_plaintext,default_member_action,default_nonmember_action
+ 0,4,0,
+ -- description
+ '',
+ -- digest_footer_uri
+ 'mailman:///$listname/$language/footer-generic.txt',
+ -- digest_header_uri
+ NULL,
+ -- digest_is_default,digest_send_periodic,digest_size_threshold
+ 0,1,30.0,
+ -- digest_volume_frequency,digestable,discard_these_nonmembers
+ 1,1,X'80025D71012E',
+ -- emergency,encode_ascii_prefixes,first_strip_reply_to
+ 0,0,0,
+ -- footer_uri
+ 'mailman:///$listname/$language/footer-generic.txt',
+ -- forward_auto_discards,gateway_to_mail,gateway_to_news
+ 1,0,0,
+ -- generic_nonmember_action,goodby_message_uri
+ 1,'',
+ -- header_matches,header_uri,hold_these_nonmembers,info,linked_newsgroup
+ X'80025D71012E',NULL,X'80025D71012E','','',
+ -- max_days_to_hold,max_message_size,max_num_recipients
+ 0,40,10,
+ -- member_moderation_notice,mime_is_default_digest,moderator_password
+ '',0,NULL,
+ -- new_member_options,news_moderation,news_prefix_subject_too
+ 256,0,1,
+ -- nntp_host,nondigestable,nonmember_rejection_notice,obscure_addresses
+ '',1,'',1,
+ -- owner_chain,owner_pipeline,personalize,post_id
+ 'default-owner-chain','default-owner-pipeline',0,1,
+ -- posting_chain,posting_pipeline,preferred_language,private_roster
+ 'default-posting-chain','default-posting-pipeline','en',1,
+ -- display_name,reject_these_nonmembers
+ 'Test',X'80025D71012E',
+ -- reply_goes_to_list,reply_to_address
+ 0,'',
+ -- require_explicit_destination,respond_to_post_requests
+ 1,1,
+ -- scrub_nondigest,send_goodbye_message,send_reminders,send_welcome_message
+ 0,1,1,1,
+ -- subject_prefix,subscribe_auto_approval
+ '[Test] ',X'80025D71012E',
+ -- subscribe_policy,topics,topics_bodylines_limit,topics_enabled
+ 1,X'80025D71012E',5,0,
+ -- unsubscribe_policy,welcome_message_uri
+ 0,'mailman:///welcome.txt');
+
+INSERT INTO "member" VALUES(
+ 1,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1',1,'test@example.com',4,NULL,5,1);
+INSERT INTO "member" VALUES(
+ 2,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd',2,'test@example.com',3,NULL,6,1);
+INSERT INTO "member" VALUES(
+ 3,'479be431-45f2-473d-bc3c-7eac614030ac',3,'test@example.com',3,NULL,7,2);
+INSERT INTO "member" VALUES(
+ 4,'e2dc604c-d93a-4b91-b5a8-749e3caade36',1,'test@example.com',4,NULL,8,2);
+
+INSERT INTO "preferences" VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(3,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(4,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(5,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(6,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(7,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+INSERT INTO "preferences" VALUES(8,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
+
+INSERT INTO "user" VALUES(
+ 1,'Anne Person',NULL,'0adf3caa-6f26-46f8-a11d-5256c8148592',
+ '2012-04-19 00:49:42.370493',1,1);
+INSERT INTO "user" VALUES(
+ 2,'Bart Person',NULL,'63f5d1a2-e533-4055-afe4-475dec3b1163',
+ '2012-04-19 00:49:52.868746',2,3);
+
+INSERT INTO "uid" VALUES(1,'8bf9a615-f23e-4980-b7d1-90ac0203c66f');
+INSERT INTO "uid" VALUES(2,'0adf3caa-6f26-46f8-a11d-5256c8148592');
+INSERT INTO "uid" VALUES(3,'63f5d1a2-e533-4055-afe4-475dec3b1163');
+INSERT INTO "uid" VALUES(4,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1');
+INSERT INTO "uid" VALUES(5,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd');
+INSERT INTO "uid" VALUES(6,'479be431-45f2-473d-bc3c-7eac614030ac');
+INSERT INTO "uid" VALUES(7,'e2dc604c-d93a-4b91-b5a8-749e3caade36');
diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py
new file mode 100644
index 000000000..f50ce35d3
--- /dev/null
+++ b/src/mailman/database/tests/test_migrations.py
@@ -0,0 +1,369 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test schema migrations."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestMigration20120407MigratedData',
+ 'TestMigration20120407Schema',
+ 'TestMigration20120407UnchangedData',
+ ]
+
+
+import unittest
+
+from pkg_resources import resource_string
+from storm.exceptions import DatabaseError
+from zope.component import getUtility
+
+from mailman.interfaces.database import IDatabaseFactory
+from mailman.interfaces.domain import IDomainManager
+from mailman.interfaces.archiver import ArchivePolicy
+from mailman.interfaces.listmanager import IListManager
+from mailman.interfaces.mailinglist import IAcceptableAliasSet
+from mailman.interfaces.nntp import NewsgroupModeration
+from mailman.interfaces.subscriptions import ISubscriptionService
+from mailman.testing.helpers import temporary_db
+from mailman.testing.layers import ConfigLayer
+
+
+
+class MigrationTestBase(unittest.TestCase):
+ """Test the dated migration (LP: #971013)
+
+ Circa: 3.0b1 -> 3.0b2
+
+ table mailinglist:
+ * news_moderation -> newsgroup_moderation
+ * news_prefix_subject_too -> nntp_prefix_subject_too
+ * include_list_post_header -> allow_list_posts
+ * ADD archive_policy
+ * ADD list_id
+ * REMOVE archive
+ * REMOVE archive_private
+ * REMOVE archive_volume_frequency
+ * REMOVE nntp_host
+
+ table member:
+ * mailing_list -> list_id
+ """
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._database = getUtility(IDatabaseFactory, 'temporary').create()
+
+ def tearDown(self):
+ self._database._cleanup()
+
+
+
+class TestMigration20120407Schema(MigrationTestBase):
+ """Test column migrations."""
+
+ def test_pre_upgrade_columns_migration(self):
+ # Test that before the migration, the old table columns are present
+ # and the new database columns are not.
+ #
+ # Load all the migrations to just before the one we're testing.
+ self._database.load_migrations('20120406999999')
+ self._database.store.commit()
+ # Verify that the database has not yet been migrated.
+ for missing in ('allow_list_posts',
+ 'archive_policy',
+ 'list_id',
+ 'nntp_prefix_subject_too'):
+ self.assertRaises(DatabaseError,
+ self._database.store.execute,
+ 'select {0} from mailinglist;'.format(missing))
+ self._database.store.rollback()
+ self.assertRaises(DatabaseError,
+ self._database.store.execute,
+ 'select list_id from member;')
+ self._database.store.rollback()
+ for present in ('archive',
+ 'archive_private',
+ 'archive_volume_frequency',
+ 'generic_nonmember_action',
+ 'include_list_post_header',
+ 'news_moderation',
+ 'news_prefix_subject_too',
+ 'nntp_host'):
+ # This should not produce an exception. Is there some better test
+ # that we can perform?
+ self._database.store.execute(
+ 'select {0} from mailinglist;'.format(present))
+ # Again, this should not produce an exception.
+ self._database.store.execute('select mailing_list from member;')
+
+ def test_post_upgrade_columns_migration(self):
+ # Test that after the migration, the old table columns are missing
+ # and the new database columns are present.
+ #
+ # Load all the migrations up to and including the one we're testing.
+ self._database.load_migrations('20120406999999')
+ self._database.load_migrations('20120407000000')
+ # Verify that the database has been migrated.
+ for present in ('allow_list_posts',
+ 'archive_policy',
+ 'list_id',
+ 'nntp_prefix_subject_too'):
+ # This should not produce an exception. Is there some better test
+ # that we can perform?
+ self._database.store.execute(
+ 'select {0} from mailinglist;'.format(present))
+ self._database.store.execute('select list_id from member;')
+ for missing in ('archive',
+ 'archive_private',
+ 'archive_volume_frequency',
+ 'generic_nonmember_action',
+ 'include_list_post_header',
+ 'news_moderation',
+ 'news_prefix_subject_too',
+ 'nntp_host'):
+ self.assertRaises(DatabaseError,
+ self._database.store.execute,
+ 'select {0} from mailinglist;'.format(missing))
+ self._database.store.rollback()
+ self.assertRaises(DatabaseError,
+ self._database.store.execute,
+ 'select mailing_list from member;')
+
+
+
+class TestMigration20120407UnchangedData(MigrationTestBase):
+ """Test non-migrated data."""
+
+ def setUp(self):
+ MigrationTestBase.setUp(self)
+ # Load all the migrations to just before the one we're testing.
+ self._database.load_migrations('20120406999999')
+ # Load the previous schema's sample data.
+ sample_data = resource_string(
+ 'mailman.database.tests.data',
+ 'migration_{0}_1.sql'.format(self._database.TAG))
+ self._database.load_sql(self._database.store, sample_data)
+ # Update to the current migration we're testing.
+ self._database.load_migrations('20120407000000')
+
+ def test_migration_domains(self):
+ # Test that the domains table, which isn't touched, doesn't change.
+ with temporary_db(self._database):
+ # Check that the domains survived the migration. This table
+ # was not touched so it should be fine.
+ domains = list(getUtility(IDomainManager))
+ self.assertEqual(len(domains), 1)
+ self.assertEqual(domains[0].mail_host, 'example.com')
+
+ def test_migration_mailing_lists(self):
+ # Test that the mailing lists survive migration.
+ with temporary_db(self._database):
+ # There should be exactly one mailing list defined.
+ mlists = list(getUtility(IListManager).mailing_lists)
+ self.assertEqual(len(mlists), 1)
+ self.assertEqual(mlists[0].fqdn_listname, 'test@example.com')
+
+ def test_migration_acceptable_aliases(self):
+ # Test that the mailing list's acceptable aliases survive migration.
+ # This proves that foreign key references are migrated properly.
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ aliases_set = IAcceptableAliasSet(mlist)
+ self.assertEqual(set(aliases_set.aliases),
+ set(['foo@example.com', 'bar@example.com']))
+
+ def test_migration_members(self):
+ # Test that the members of a mailing list all survive migration.
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ # Test that all the members we expect are still there. Start with
+ # the two list delivery members.
+ addresses = set(address.email
+ for address in mlist.members.addresses)
+ self.assertEqual(addresses,
+ set(['anne@example.com', 'bart@example.com']))
+ # There is one owner.
+ owners = set(address.email for address in mlist.owners.addresses)
+ self.assertEqual(len(owners), 1)
+ self.assertEqual(owners.pop(), 'anne@example.com')
+ # There is one moderator.
+ moderators = set(address.email
+ for address in mlist.moderators.addresses)
+ self.assertEqual(len(moderators), 1)
+ self.assertEqual(moderators.pop(), 'bart@example.com')
+
+
+
+class TestMigration20120407MigratedData(MigrationTestBase):
+ """Test affected migration data."""
+
+ def setUp(self):
+ MigrationTestBase.setUp(self)
+ # Load all the migrations to just before the one we're testing.
+ self._database.load_migrations('20120406999999')
+ # Load the previous schema's sample data.
+ sample_data = resource_string(
+ 'mailman.database.tests.data',
+ 'migration_{0}_1.sql'.format(self._database.TAG))
+ self._database.load_sql(self._database.store, sample_data)
+
+ def _upgrade(self):
+ # Update to the current migration we're testing.
+ self._database.load_migrations('20120407000000')
+
+ def test_migration_archive_policy_never_0(self):
+ # Test that the new archive_policy value is updated correctly. In the
+ # case of old column archive=0, the archive_private column is
+ # ignored. This test sets it to 0 to ensure it's ignored.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET archive = {0}, archive_private = {0} '
+ 'WHERE id = 1;'.format(self._database.FALSE))
+ # Complete the migration
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.archive_policy, ArchivePolicy.never)
+
+ def test_migration_archive_policy_never_1(self):
+ # Test that the new archive_policy value is updated correctly. In the
+ # case of old column archive=0, the archive_private column is
+ # ignored. This test sets it to 1 to ensure it's ignored.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET archive = {0}, archive_private = {1} '
+ 'WHERE id = 1;'.format(self._database.FALSE,
+ self._database.TRUE))
+ # Complete the migration
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.archive_policy, ArchivePolicy.never)
+
+ def test_archive_policy_private(self):
+ # Test that the new archive_policy value is updated correctly for
+ # private archives.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET archive = {0}, archive_private = {0} '
+ 'WHERE id = 1;'.format(self._database.TRUE))
+ # Complete the migration
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.archive_policy, ArchivePolicy.private)
+
+ def test_archive_policy_public(self):
+ # Test that the new archive_policy value is updated correctly for
+ # public archives.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET archive = {1}, archive_private = {0} '
+ 'WHERE id = 1;'.format(self._database.FALSE,
+ self._database.TRUE))
+ # Complete the migration
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.archive_policy, ArchivePolicy.public)
+
+ def test_list_id(self):
+ # Test that the mailinglist table gets a list_id column.
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.list_id, 'test.example.com')
+
+ def test_list_id_member(self):
+ # Test that the member table's mailing_list column becomes list_id.
+ self._upgrade()
+ with temporary_db(self._database):
+ service = getUtility(ISubscriptionService)
+ members = list(service.find_members(list_id='test.example.com'))
+ self.assertEqual(len(members), 4)
+
+ def test_news_moderation_none(self):
+ # Test that news_moderation becomes newsgroup_moderation.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET news_moderation = 0 '
+ 'WHERE id = 1;')
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.newsgroup_moderation,
+ NewsgroupModeration.none)
+
+ def test_news_moderation_open_moderated(self):
+ # Test that news_moderation becomes newsgroup_moderation.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET news_moderation = 1 '
+ 'WHERE id = 1;')
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.newsgroup_moderation,
+ NewsgroupModeration.open_moderated)
+
+ def test_news_moderation_moderated(self):
+ # Test that news_moderation becomes newsgroup_moderation.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET news_moderation = 2 '
+ 'WHERE id = 1;')
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertEqual(mlist.newsgroup_moderation,
+ NewsgroupModeration.moderated)
+
+ def test_nntp_prefix_subject_too_false(self):
+ # Test that news_prefix_subject_too becomes nntp_prefix_subject_too.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET news_prefix_subject_too = {0} '
+ 'WHERE id = 1;'.format(self._database.FALSE))
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertFalse(mlist.nntp_prefix_subject_too)
+
+ def test_nntp_prefix_subject_too_true(self):
+ # Test that news_prefix_subject_too becomes nntp_prefix_subject_too.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET news_prefix_subject_too = {0} '
+ 'WHERE id = 1;'.format(self._database.TRUE))
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertTrue(mlist.nntp_prefix_subject_too)
+
+ def test_allow_list_posts_false(self):
+ # Test that include_list_post_header -> allow_list_posts.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET include_list_post_header = {0} '
+ 'WHERE id = 1;'.format(self._database.FALSE))
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertFalse(mlist.allow_list_posts)
+
+ def test_allow_list_posts_true(self):
+ # Test that include_list_post_header -> allow_list_posts.
+ self._database.store.execute(
+ 'UPDATE mailinglist SET include_list_post_header = {0} '
+ 'WHERE id = 1;'.format(self._database.TRUE))
+ self._upgrade()
+ with temporary_db(self._database):
+ mlist = getUtility(IListManager).get('test@example.com')
+ self.assertTrue(mlist.allow_list_posts)
diff --git a/src/mailman/database/transaction.py b/src/mailman/database/transaction.py
index 47e58d3e2..295f3d567 100644
--- a/src/mailman/database/transaction.py
+++ b/src/mailman/database/transaction.py
@@ -17,19 +17,36 @@
"""Transactional support."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
- 'txn',
+ 'dbconnection',
+ 'transaction',
+ 'transactional',
]
+from contextlib import contextmanager
+
from mailman.config import config
-class txn(object):
+@contextmanager
+def transaction():
+ """Context manager for ensuring the transaction is complete."""
+ try:
+ yield
+ except:
+ config.db.abort()
+ raise
+ else:
+ config.db.commit()
+
+
+
+def transactional(function):
"""Decorator for transactional support.
When the function this decorator wraps exits cleanly, the current
@@ -38,16 +55,25 @@ class txn(object):
Either way, the current transaction is completed.
"""
- def __init__(self, function):
- self._function = function
+ def wrapper(*args, **kws):
+ try:
+ rtn = function(*args, **kws)
+ config.db.commit()
+ return rtn
+ except:
+ config.db.abort()
+ raise
+ return wrapper
- def __get__(self, obj, type=None):
- def wrapper(*args, **kws):
- try:
- rtn = self._function(obj, *args, **kws)
- config.db.commit()
- return rtn
- except:
- config.db.abort()
- raise
- return wrapper
+
+
+def dbconnection(function):
+ """Decorator for getting at the database connection.
+
+ Use this to avoid having to access the global `config.db.store`
+ attribute. This calls the function with `store` as the first argument.
+ """
+ def wrapper(*args, **kws):
+ # args[0] is self.
+ return function(args[0], config.db.store, *args[1:], **kws)
+ return wrapper
diff --git a/src/mailman/docs/ACKNOWLEDGMENTS.rst b/src/mailman/docs/ACKNOWLEDGMENTS.rst
index b6c6cc16c..10d4bcd93 100644
--- a/src/mailman/docs/ACKNOWLEDGMENTS.rst
+++ b/src/mailman/docs/ACKNOWLEDGMENTS.rst
@@ -58,6 +58,7 @@ code, and have assigned copyright for contributions to the FSF:
* Simone Piunno
* Claudia Schmidt
* Andreas Schosser
+* Stephen J. Turnbull
* Richard Wackerbarth
@@ -247,6 +248,7 @@ left off the list!
* Greg Stein
* Dale Stimson
* Students of HIT <mailman-cn@mail.cs.hit.edu.cn>
+* Alexander Sulfrian
* Szabolcs Szigeti
* Vizi Szilard
* David T-G
diff --git a/src/mailman/docs/DATABASE.rst b/src/mailman/docs/DATABASE.rst
index f5fe39849..762d60a11 100644
--- a/src/mailman/docs/DATABASE.rst
+++ b/src/mailman/docs/DATABASE.rst
@@ -8,10 +8,10 @@ however, Storm is compatible with PostgreSQL_ and MySQL, among possibly
others.
Currently, Mailman is known to work with either the default SQLite3 database,
-or PostgreSQL. (Volunteers to port it to MySQL are welcome!). If you want to
-use SQLite3, you generally don't need to change anything, but if you want
-Mailman to use PostgreSQL, you'll need to set that up first, and then change a
-configuration variable in your `/etc/mailman.cfg` file.
+or PostgreSQL. (Volunteers to port it to other databases are welcome!). If
+you want to use SQLite3, you generally don't need to change anything, but if
+you want Mailman to use PostgreSQL, you'll need to set that up first, and then
+change a configuration variable in your `/etc/mailman.cfg` file.
Two configuration variables control which database Mailman uses. The first
names the class implementing the database interface. The second names the
diff --git a/src/mailman/docs/INTRODUCTION.rst b/src/mailman/docs/INTRODUCTION.rst
index fd64efb42..11d13c239 100644
--- a/src/mailman/docs/INTRODUCTION.rst
+++ b/src/mailman/docs/INTRODUCTION.rst
@@ -53,8 +53,8 @@ Warsaw, Ken Manheimer, Scott Cotton, Harald Meland, and John Viega. Version
1.0 and beyond have been primarily maintained by Barry Warsaw with
contributions from many; see the ACKNOWLEDGMENTS file for details. Jeremy
Hylton helped considerably with the Pipermail code in Mailman 2.0. Mailman
-2.1 is primarily maintained by Mark Sapiro and Tokio Kikuchi. Barry Warsaw is
-the lead developer on Mailman 3.
+2.1 is primarily maintained by Mark Sapiro, with previous help by Tokio
+Kikuchi. Barry Warsaw is the lead developer on Mailman 3.
Help
diff --git a/src/mailman/docs/MTA.rst b/src/mailman/docs/MTA.rst
index f541d3838..c6d2230c4 100644
--- a/src/mailman/docs/MTA.rst
+++ b/src/mailman/docs/MTA.rst
@@ -2,9 +2,10 @@
Hooking up your mail server
===========================
-Mailman needs to be hooked up to your mail server both to accept incoming mail
-and to deliver outgoing mail. Mailman itself never delivers messages to the
-end user; it lets its immediate upstream mail server do that.
+Mailman needs to be hooked up to your mail server (a.k.a. *mail transport
+agent* or *MTA*) both to accept incoming mail and to deliver outgoing mail.
+Mailman itself never delivers messages to the end user; it lets its immediate
+upstream mail server do that.
The preferred way to allow Mailman to accept incoming messages from your mail
server is to use the `Local Mail Transfer Protocol`_ (LMTP_) interface. Most
@@ -32,6 +33,9 @@ Contributions are welcome!
Postfix
=======
+Postfix_ is an open source mail server by Wietse Venema.
+
+
Mailman settings
----------------
@@ -58,9 +62,9 @@ as shown above.
Basic Postfix connections
-------------------------
-There are several ways to hook Postfix_ up to Mailman, so here are the
-simplest instructions. The following settings should be added to Postfix's
-`main.cf` file.
+There are several ways to hook Postfix up to Mailman, so here are the simplest
+instructions. The following settings should be added to Postfix's `main.cf`
+file.
Mailman supports a technique called `Variable Envelope Return Path`_ (VERP) to
disambiguate and accurately record bounces. By default Mailman's VERP
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index b92d57068..5020b89e3 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -10,10 +10,26 @@ Here is a history of user visible changes to Mailman.
3.0 beta 2 -- "Freeze"
======================
-(20XX-XX-XX)
+(2012-09-05)
Architecture
------------
+ * The link between members and the mailing lists they are subscribed to, is
+ now via the RFC 2369 `list_id` instead of the fqdn listname (i.e. posting
+ address). This is because while the posting address can change if the
+ mailing list is moved to a new server, the list id is fixed.
+ (LP: #1024509)
+
+ - IListManager.get_by_list_id() added.
+ - IListManager.list_ids added.
+ - IMailingList.list_id added.
+ - Several internal APIs that accepted fqdn list names now require list ids,
+ e.g. ISubscriptionService.join() and .find_members().
+ - IMember.list_id attribute added; .mailing_list is now an alias that
+ retrieves and returns the IMailingList.
+
+ * `passlib`_ is now used for all password hashing instead of flufl.password.
+ The default hash is `sha512_crypt`. (LP: #1015758)
* Internally, all datetimes are kept in the UTC timezone, however because of
LP: #280708, they are stored in the database in naive format.
* `received_time` is now added to the message metadata by the LMTP runner
@@ -31,6 +47,52 @@ Architecture
or unverified. (LP: #975698)
* A `PasswordChangeEvent` is triggered when an `IUser`'s password changes.
(LP: #975700)
+ * When a queue runner gets an exception in its _dispose() method, a
+ `RunnerCrashEvent` is triggered, which contains references to the queue
+ runner, mailing list, message, metadata, and exception. Interested parties
+ can subscribe to that `zope.event` for notification.
+ * Events renamed and moved:
+ * `mailman.chains.accept.AcceptNotification`
+ * `mailman.chains.base.ChainNotification`
+ * `mailman.chains.discard.DiscardNotification`
+ * `mailman.chains.hold.HoldNotification`
+ * `mailman.chains.owner.OwnerNotification`
+ * `mailman.chains.reject.RejectNotification`
+ changed to (respectively):
+ * `mailman.interfaces.chains.AcceptEvent`
+ * `mailman.interfaces.chains.ChainEvent`
+ * `mailman.interfaces.chains.DiscardEvent`
+ * `mailman.interfaces.chains.HoldEvent`
+ * `mailman.interfaces.chains.AcceptOwnerEvent`
+ * `mailman.interfaces.chains.RejectEvent`
+ * A `ConfigurationUpdatedEvent` is triggered when the system-wide global
+ configuration stack is pushed or popped.
+ * The policy for archiving has now been collapsed into a single enum, called
+ ArchivePolicy. This describes the three states of never archive, archive
+ privately, and archive_publicly. (LP: #967238)
+
+Database
+--------
+ * Schema migrations (LP: #971013)
+
+ - mailinglist.include_list_post_header -> allow_list_posts
+ - mailinglist.news_prefix_subject_too -> nntp_prefix_subject_too
+ - mailinglist.news_moderation -> newsgroup_moderation
+ - mailinglist.archive and mailinglist.archive_private have been collapsed
+ into archive_policy.
+ - mailinglist.nntp_host has been removed.
+ - mailinglist.generic_nonmember_action has been removed (LP: #975696)
+
+ * Schema migrations (LP: #1024509)
+ - member.mailing_list -> list_id
+ * The PostgreSQL port of the schema accidentally added a moderation_callback
+ column to the mailinglist table. Since this is unused in Mailman, it was
+ simply commented out of the base schema for PostgreSQL.
+
+REST
+----
+ * Expose `archive_policy` in the REST API. Contributed by Alexander
+ Sulfrian. (LP: #1039129)
Configuration
-------------
@@ -38,9 +100,12 @@ Configuration
every `[archiver.<name>]` section. These are used to determine under what
circumstances a message destined for a specific archiver should have its
`Date:` header clobbered. (LP: #963612)
+ * With the switch to `passlib`_, `[passwords]password_scheme` has been
+ removed. Instead use `[passwords]path` to specify where to find the
+ `passlib.cfg` file. See the comments in `schema.cfg` for details.
* Configuration schema variable changes:
- [nntp]username -> [nntp]user
- [nntp]port (added)
+ * [nntp]username -> [nntp]user
+ * [nntp]port (added)
* Header check specifications in the `mailman.cfg` file have changed quite
bit. The previous `[spam.header.foo]` sections have been removed.
Instead, there's a new `[antispam]` section that contains a `header_checks`
@@ -61,6 +126,13 @@ Bug fixes
given by Mark Sapiro. (LP: #949924)
* Fixed a typo when returning the configuration file's header match checks.
(LP: #953497)
+ * List-Post should be NO when posting is not allowed. (LP: #987563)
+ * Non-unicode values in msgdata broke pending requests. (LP: #1031391)
+ * Show devmode in `bin/mailman info` output. (LP: #1035028)
+ * Fix residual references to the old `IMailingList` archive variables.
+ (LP: #1031393)
+
+.. _`passlib`: http://packages.python.org/passlib/index.html
3.0 beta 1 -- "The Twilight Zone"
diff --git a/src/mailman/docs/START.rst b/src/mailman/docs/START.rst
index 57df48e30..da76feae0 100644
--- a/src/mailman/docs/START.rst
+++ b/src/mailman/docs/START.rst
@@ -9,11 +9,8 @@ Beta Release
============
This is a beta release. The developers believe it has sufficient
-functionality to provide full services to a mailing list, but it is not ready
-for production yet. Interfaces and administration may differ substantially
-from the alpha series, but changes should be incremental going forward from
-beta 1. Changes from the alpha series will be described in notes to the main
-text.
+functionality to provide full mailing list services, but it is not yet ready
+for production use.
The Mailman 3 beta releases are being provided to give developers and other
interested people an early look at the next major version, and site
@@ -40,9 +37,9 @@ $PATH or it can be accessible via the ``python2.6`` or ``python2.7`` binary.
If your operating system does not include Python, see http://www.python.org
downloading and installing it from source. Python 3 is not yet supported.
-In this documentation, a bare ``python`` refers to the python used to invoke
-``bootstrap.py``, which might be ``python2.6`` or ``python2.7``, as well as
-the system ``python`` or an absolute path.
+In this documentation, a bare ``python`` refers to the Python executable used
+to invoke ``bootstrap.py``, which might be ``python2.6`` or ``python2.7``, as
+well as the system ``python`` or an absolute path.
Mailman 3 is now based on the `zc.buildout`_ infrastructure, which greatly
simplifies building and testing Mailman.
diff --git a/src/mailman/docs/STYLEGUIDE.rst b/src/mailman/docs/STYLEGUIDE.rst
index 32b2da72f..b744c6557 100644
--- a/src/mailman/docs/STYLEGUIDE.rst
+++ b/src/mailman/docs/STYLEGUIDE.rst
@@ -2,7 +2,7 @@
GNU Mailman Coding Style Guide
==============================
-Copyright (C) 2002-2011 Barry A. Warsaw
+Copyright (C) 2002-2012 Barry A. Warsaw
Python coding style guide for GNU Mailman
diff --git a/src/mailman/email/validate.py b/src/mailman/email/validate.py
index 1861a8121..021e4c073 100644
--- a/src/mailman/email/validate.py
+++ b/src/mailman/email/validate.py
@@ -27,7 +27,7 @@ __all__ = [
import re
-from zope.interface import implements
+from zope.interface import implementer
from mailman.interfaces.address import (
IEmailValidator, InvalidEmailAddressError)
@@ -39,11 +39,10 @@ _badchars = re.compile(r'[][()<>|;^,\000-\037\177-\377]')
+@implementer(IEmailValidator)
class Validator:
"""An email address validator."""
- implements(IEmailValidator)
-
def is_valid(self, email):
"""See `IEmailValidator`."""
if not email or ' ' in email:
diff --git a/src/mailman/handlers/acknowledge.py b/src/mailman/handlers/acknowledge.py
index 0e0916337..0366f8ce6 100644
--- a/src/mailman/handlers/acknowledge.py
+++ b/src/mailman/handlers/acknowledge.py
@@ -20,7 +20,7 @@
This only happens if the sender has set their AcknowledgePosts attribute.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,7 +29,7 @@ __all__ = [
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.email.message import UserNotification
@@ -40,9 +40,9 @@ from mailman.utilities.string import oneline
+@implementer(IHandler)
class Acknowledge:
"""Send an acknowledgment."""
- implements(IHandler)
name = 'acknowledge'
description = _("""Send an acknowledgment of a posting.""")
diff --git a/src/mailman/handlers/after_delivery.py b/src/mailman/handlers/after_delivery.py
index a964804b5..0a3ba2c75 100644
--- a/src/mailman/handlers/after_delivery.py
+++ b/src/mailman/handlers/after_delivery.py
@@ -17,7 +17,7 @@
"""Perform some bookkeeping after a successful post."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,7 +25,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
@@ -33,11 +33,10 @@ from mailman.utilities.datetime import now
+@implementer(IHandler)
class AfterDelivery:
"""Perform some bookkeeping after a successful post."""
- implements(IHandler)
-
name = 'after-delivery'
description = _('Perform some bookkeeping after a successful post.')
diff --git a/src/mailman/handlers/avoid_duplicates.py b/src/mailman/handlers/avoid_duplicates.py
index ffbc80c85..de1939822 100644
--- a/src/mailman/handlers/avoid_duplicates.py
+++ b/src/mailman/handlers/avoid_duplicates.py
@@ -23,7 +23,7 @@ has already received a copy, we either drop the message, add a duplicate
warning header, or pass it through, depending on the user's preferences.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -32,7 +32,7 @@ __all__ = [
from email.utils import getaddresses, formataddr
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
@@ -42,11 +42,10 @@ COMMASPACE = ', '
+@implementer(IHandler)
class AvoidDuplicates:
"""If the user wishes it, do not send duplicates of the same message."""
- implements(IHandler)
-
name = 'avoid-duplicates'
description = _('Suppress some duplicates of the same message.')
diff --git a/src/mailman/handlers/cleanse.py b/src/mailman/handlers/cleanse.py
index 605b843d0..32d3455ca 100644
--- a/src/mailman/handlers/cleanse.py
+++ b/src/mailman/handlers/cleanse.py
@@ -17,7 +17,7 @@
"""Cleanse certain headers from all messages."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import logging
from email.utils import formataddr
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.handlers.cook_headers import uheader
@@ -39,11 +39,10 @@ log = logging.getLogger('mailman.post')
+@implementer(IHandler)
class Cleanse:
"""Cleanse certain headers from all messages."""
- implements(IHandler)
-
name = 'cleanse'
description = _('Cleanse certain headers from all messages.')
diff --git a/src/mailman/handlers/cleanse_dkim.py b/src/mailman/handlers/cleanse_dkim.py
index d2cd32636..bc23980b7 100644
--- a/src/mailman/handlers/cleanse_dkim.py
+++ b/src/mailman/handlers/cleanse_dkim.py
@@ -25,7 +25,7 @@ and it will also give the MTA the opportunity to regenerate valid keys
originating at the Mailman server for the outgoing message.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -34,7 +34,7 @@ __all__ = [
from lazr.config import as_boolean
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -42,11 +42,10 @@ from mailman.interfaces.handler import IHandler
+@implementer(IHandler)
class CleanseDKIM:
"""Remove DomainKeys headers."""
- implements(IHandler)
-
name = 'cleanse-dkim'
description = _('Remove DomainKeys headers.')
diff --git a/src/mailman/handlers/cook_headers.py b/src/mailman/handlers/cook_headers.py
index 5d1e416a6..535155ab7 100644
--- a/src/mailman/handlers/cook_headers.py
+++ b/src/mailman/handlers/cook_headers.py
@@ -17,7 +17,7 @@
"""Cook a message's headers."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -30,7 +30,7 @@ import re
from email.errors import HeaderParseError
from email.header import Header, decode_header, make_header
from email.utils import parseaddr, formataddr, getaddresses
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
@@ -279,11 +279,10 @@ def ch_oneline(headerstr):
+@implementer(IHandler)
class CookHeaders:
"""Modify message headers."""
- implements(IHandler)
-
name = 'cook-headers'
description = _('Modify message headers.')
diff --git a/src/mailman/handlers/decorate.py b/src/mailman/handlers/decorate.py
index d6d156048..c5fad2891 100644
--- a/src/mailman/handlers/decorate.py
+++ b/src/mailman/handlers/decorate.py
@@ -31,7 +31,7 @@ import logging
from email.mime.text import MIMEText
from urllib2 import URLError
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.email.message import Message
@@ -57,7 +57,7 @@ def process(mlist, msg, msgdata):
d['user_address'] = recipient
d['user_delivered_to'] = member.address.original_email
d['user_language'] = member.preferred_language.description
- d['user_name'] = (member.user.display_name
+ d['user_name'] = (member.user.display_name
if member.user.display_name
else member.address.original_email)
d['user_optionsurl'] = member.options_url
@@ -232,11 +232,10 @@ def decorate(mlist, uri, extradict=None):
+@implementer(IHandler)
class Decorate:
"""Decorate a message with headers and footers."""
- implements(IHandler)
-
name = 'decorate'
description = _('Decorate a message with headers and footers.')
diff --git a/src/mailman/handlers/docs/archives.rst b/src/mailman/handlers/docs/archives.rst
index 323d121e8..abaad9f52 100644
--- a/src/mailman/handlers/docs/archives.rst
+++ b/src/mailman/handlers/docs/archives.rst
@@ -26,7 +26,8 @@ should *not* get archived.
For example, no digests should ever get archived.
- >>> mlist.archive = True
+ >>> from mailman.interfaces.archiver import ArchivePolicy
+ >>> mlist.archive_policy = ArchivePolicy.public
>>> msg = message_from_string("""\
... Subject: A sample message
...
@@ -39,7 +40,7 @@ For example, no digests should ever get archived.
If the mailing list is not configured to archive, then even regular deliveries
won't be archived.
- >>> mlist.archive = False
+ >>> mlist.archive_policy = ArchivePolicy.never
>>> handler.process(mlist, msg, {})
>>> switchboard.files
[]
@@ -49,7 +50,7 @@ want to be archived. We've seen both in the wild so both are supported. The
``X-No-Archive:`` header can be used to indicate that the message should not
be archived. Confusingly, this header's value is actually ignored.
- >>> mlist.archive = True
+ >>> mlist.archive_policy = ArchivePolicy.public
>>> msg = message_from_string("""\
... Subject: A sample message
... X-No-Archive: YES
diff --git a/src/mailman/handlers/docs/rfc-2369.rst b/src/mailman/handlers/docs/rfc-2369.rst
index 0461f27ba..7064de6bd 100644
--- a/src/mailman/handlers/docs/rfc-2369.rst
+++ b/src/mailman/handlers/docs/rfc-2369.rst
@@ -7,7 +7,8 @@ headers generally start with the `List-` prefix.
>>> mlist = create_list('test@example.com')
>>> mlist.preferred_language = 'en'
- >>> mlist.archive = False
+ >>> from mailman.interfaces.archiver import ArchivePolicy
+ >>> mlist.archive_policy = ArchivePolicy.never
..
This is a helper function for the following section.
@@ -59,13 +60,14 @@ about hiding them. A list owner can turn these headers off.
Messages which Mailman generates itself, such as user or owner notifications,
have a reduced set of `List-` headers. Specifically, there is no `List-Post`,
`List-Archive` or `Archived-At` header.
+..
>>> mlist.include_rfc2369_headers = True
- >>> mlist.include_list_post_header = False
>>> msg = message_from_string("""\
... From: aperson@example.com
...
... """)
+
>>> process(mlist, msg, dict(reduced_list_headers=True))
>>> list_headers(msg)
---start---
@@ -84,7 +86,8 @@ List-Post header
Discussion lists, to which any subscriber can post, also have a `List-Post`
header which contains the `mailto:` URL used to send messages to the list.
- >>> mlist.include_list_post_header = True
+ >>> mlist.include_rfc2369_headers = True
+ >>> mlist.allow_list_posts = True
>>> msg = message_from_string("""\
... From: aperson@example.com
...
@@ -101,6 +104,28 @@ header which contains the `mailto:` URL used to send messages to the list.
<mailto:test-leave@example.com>
---end---
+Some mailing lists are announce, or one-way lists, not discussion lists.
+Because the general membership cannot post to these mailing lists, the list
+owner can set a flag which adds a special `List-Post` header value, according
+to RFC 2369.
+
+ >>> mlist.allow_list_posts = False
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ list-help: <mailto:test-request@example.com?subject=help>
+ list-id: <test.example.com>
+ list-post: NO
+ list-subscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-join@example.com>
+ list-unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-leave@example.com>
+ ---end---
+
List-Id header
==============
@@ -108,6 +133,7 @@ List-Id header
If the mailing list has a description, then it is included in the ``List-Id``
header.
+ >>> mlist.allow_list_posts = True
>>> mlist.description = 'My test mailing list'
>>> msg = message_from_string("""\
... From: aperson@example.com
@@ -146,7 +172,7 @@ Archive headers
When the mailing list is configured to enable archiving, a `List-Archive`
header will be added.
- >>> mlist.archive = True
+ >>> mlist.archive_policy = ArchivePolicy.public
`RFC 5064`_ defines the `Archived-At` header which contains the url to the
individual message in the archives. Archivers which don't support
@@ -183,7 +209,7 @@ If the mailing list isn't being archived, neither the `List-Archive` nor
`Archived-At` headers will be added.
>>> config.pop('prototype')
- >>> mlist.archive = False
+ >>> mlist.archive_policy = ArchivePolicy.never
>>> msg = message_from_string("""\
... From: aperson@example.com
...
diff --git a/src/mailman/handlers/file_recipients.py b/src/mailman/handlers/file_recipients.py
index d087ff2bb..750357a90 100644
--- a/src/mailman/handlers/file_recipients.py
+++ b/src/mailman/handlers/file_recipients.py
@@ -17,7 +17,7 @@
"""Get the normal delivery recipients from a Sendmail style :include: file."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,18 +28,17 @@ __all__ = [
import os
import errno
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
+@implementer(IHandler)
class FileRecipients:
"""Get the normal delivery recipients from an include file."""
- implements(IHandler)
-
name = 'file-recipients'
description = _('Get the normal delivery recipients from an include file.')
diff --git a/src/mailman/handlers/member_recipients.py b/src/mailman/handlers/member_recipients.py
index 956ea6adc..ec8ed77b1 100644
--- a/src/mailman/handlers/member_recipients.py
+++ b/src/mailman/handlers/member_recipients.py
@@ -31,7 +31,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core import errors
@@ -42,11 +42,10 @@ from mailman.utilities.string import wrap
+@implementer(IHandler)
class MemberRecipients:
"""Calculate the regular (i.e. non-digest) recipients of the message."""
- implements(IHandler)
-
name = 'member-recipients'
description = _('Calculate the regular recipients of the message.')
diff --git a/src/mailman/handlers/mime_delete.py b/src/mailman/handlers/mime_delete.py
index c9c1eb408..52fcc99fa 100644
--- a/src/mailman/handlers/mime_delete.py
+++ b/src/mailman/handlers/mime_delete.py
@@ -24,7 +24,7 @@ wrapping only single sections after other processing are replaced by their
contents.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -42,7 +42,7 @@ from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from lazr.config import as_boolean
from os.path import splitext
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core import errors
@@ -285,11 +285,10 @@ def get_file_ext(m):
+@implementer(IHandler)
class MIMEDelete:
"""Filter the MIME content of messages."""
- implements(IHandler)
-
name = 'mime-delete'
description = _('Filter the MIME content of messages.')
diff --git a/src/mailman/handlers/owner_recipients.py b/src/mailman/handlers/owner_recipients.py
index e431d00cf..66c380635 100644
--- a/src/mailman/handlers/owner_recipients.py
+++ b/src/mailman/handlers/owner_recipients.py
@@ -25,7 +25,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -34,11 +34,10 @@ from mailman.interfaces.member import DeliveryStatus
+@implementer(IHandler)
class OwnerRecipients:
"""Calculate the owner (and moderator) recipients for -owner postings."""
- implements(IHandler)
-
name = 'owner-recipients'
description = _('Calculate the owner and moderator recipients.')
diff --git a/src/mailman/handlers/replybot.py b/src/mailman/handlers/replybot.py
index 83aa40214..a25f4f30b 100644
--- a/src/mailman/handlers/replybot.py
+++ b/src/mailman/handlers/replybot.py
@@ -17,7 +17,7 @@
"""Handler for automatic responses."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import logging
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.email.message import UserNotification
@@ -44,11 +44,10 @@ log = logging.getLogger('mailman.error')
+@implementer(IHandler)
class Replybot:
"""Send automatic responses."""
- implements(IHandler)
-
name = 'replybot'
description = _('Send automatic responses.')
diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index ece4e83cb..47dd9dad2 100644
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -17,7 +17,7 @@
"""RFC 2369 List-* and related headers."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,11 +26,12 @@ __all__ = [
from email.utils import formataddr
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
from mailman.handlers.cook_headers import uheader
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.handler import IHandler
@@ -74,11 +75,15 @@ def process(mlist, msg, msgdata):
'List-Subscribe' : subfieldfmt.format(listinfo, mlist.join_address),
})
if not msgdata.get('reduced_list_headers'):
- # List-Post: is controlled by a separate attribute
- if mlist.include_list_post_header:
- headers['List-Post'] = '<mailto:{0}>'.format(mlist.posting_address)
+ # List-Post: is controlled by a separate attribute, which is somewhat
+ # misnamed. RFC 2369 requires a value of NO if posting is not
+ # allowed, i.e. for an announce-only list.
+ list_post = ('<mailto:{0}>'.format(mlist.posting_address)
+ if mlist.allow_list_posts
+ else 'NO')
+ headers['List-Post'] = list_post
# Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
- if mlist.archive:
+ if mlist.archive_policy is not ArchivePolicy.never:
for archiver in config.archivers:
headers['List-Archive'] = '<{0}>'.format(
archiver.list_url(mlist))
@@ -100,11 +105,10 @@ def process(mlist, msg, msgdata):
+@implementer(IHandler)
class RFC2369:
"""Add the RFC 2369 List-* headers."""
- implements(IHandler)
-
name = 'rfc-2369'
description = _('Add the RFC 2369 List-* headers.')
diff --git a/src/mailman/handlers/tagger.py b/src/mailman/handlers/tagger.py
index 49e004a12..9d78372e6 100644
--- a/src/mailman/handlers/tagger.py
+++ b/src/mailman/handlers/tagger.py
@@ -17,7 +17,7 @@
"""Extract topics from the original mail message."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,7 +29,7 @@ import re
import email.iterators
import email.parser
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
@@ -178,11 +178,10 @@ class _ForgivingParser(email.parser.HeaderParser):
+@implementer(IHandler)
class Tagger:
"""Tag messages with topic matches."""
- implements(IHandler)
-
name = 'tagger'
description = _('Tag messages with topic matches.')
diff --git a/src/mailman/handlers/to_archive.py b/src/mailman/handlers/to_archive.py
index fd5259a14..6b82aef61 100644
--- a/src/mailman/handlers/to_archive.py
+++ b/src/mailman/handlers/to_archive.py
@@ -17,7 +17,7 @@
"""Add the message to the archives."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,26 +25,27 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.handler import IHandler
+@implementer(IHandler)
class ToArchive:
"""Add the message to the archives."""
- implements(IHandler)
-
name = 'to-archive'
description = _('Add the message to the archives.')
def process(self, mlist, msg, msgdata):
"""See `IHandler`."""
# Short circuits.
- if msgdata.get('isdigest') or not mlist.archive:
+ if (msgdata.get('isdigest') or
+ mlist.archive_policy is ArchivePolicy.never):
return
# Common practice seems to favor "X-No-Archive: yes". No other value
# for this header seems to make sense, so we'll just test for it's
diff --git a/src/mailman/handlers/to_digest.py b/src/mailman/handlers/to_digest.py
index 71511f136..8067e2c0c 100644
--- a/src/mailman/handlers/to_digest.py
+++ b/src/mailman/handlers/to_digest.py
@@ -17,7 +17,7 @@
"""Add the message to the list's current digest."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
import os
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -39,11 +39,10 @@ from mailman.utilities.mailbox import Mailbox
+@implementer(IHandler)
class ToDigest:
"""Add the message to the digest, possibly sending it."""
- implements(IHandler)
-
name = 'to-digest'
description = _('Add the message to the digest, possibly sending it.')
diff --git a/src/mailman/handlers/to_outgoing.py b/src/mailman/handlers/to_outgoing.py
index 971f87757..a212485c7 100644
--- a/src/mailman/handlers/to_outgoing.py
+++ b/src/mailman/handlers/to_outgoing.py
@@ -22,7 +22,7 @@ posted to the list membership. Anything else that needs to go out to some
recipient should just be placed in the out queue directly.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -30,7 +30,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -38,11 +38,10 @@ from mailman.interfaces.handler import IHandler
+@implementer(IHandler)
class ToOutgoing:
"""Send the message to the outgoing queue."""
- implements(IHandler)
-
name = 'to-outgoing'
description = _('Send the message to the outgoing queue.')
diff --git a/src/mailman/handlers/to_usenet.py b/src/mailman/handlers/to_usenet.py
index 021f8f9e5..79f4c9b1b 100644
--- a/src/mailman/handlers/to_usenet.py
+++ b/src/mailman/handlers/to_usenet.py
@@ -27,7 +27,7 @@ __all__ = [
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -39,11 +39,10 @@ log = logging.getLogger('mailman.error')
+@implementer(IHandler)
class ToUsenet:
"""Move the message to the outgoing news queue."""
- implements(IHandler)
-
name = 'to-usenet'
description = _('Move the message to the outgoing news queue.')
diff --git a/src/mailman/interfaces/archiver.py b/src/mailman/interfaces/archiver.py
index f3edc7719..d9ca45514 100644
--- a/src/mailman/interfaces/archiver.py
+++ b/src/mailman/interfaces/archiver.py
@@ -17,10 +17,11 @@
"""Interface for archiving schemes."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'ArchivePolicy',
'ClobberDate',
'IArchiver',
]
@@ -31,6 +32,13 @@ from zope.interface import Interface, Attribute
+class ArchivePolicy(Enum):
+ never = 0
+ private = 1
+ public = 2
+
+
+
class ClobberDate(Enum):
never = 1
maybe = 2
diff --git a/src/mailman/interfaces/chain.py b/src/mailman/interfaces/chain.py
index 8858f874c..8862026f8 100644
--- a/src/mailman/interfaces/chain.py
+++ b/src/mailman/interfaces/chain.py
@@ -21,11 +21,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'AcceptEvent',
+ 'AcceptOwnerEvent',
+ 'ChainEvent',
+ 'DiscardEvent',
+ 'HoldEvent',
'IChain',
'IChainIterator',
'IChainLink',
'IMutableChain',
'LinkAction',
+ 'RejectEvent',
]
@@ -34,6 +40,37 @@ from zope.interface import Interface, Attribute
+class ChainEvent:
+ """Base class for chain notification events."""
+
+ def __init__(self, mlist, msg, msgdata, chain):
+ self.mlist = mlist
+ self.msg = msg
+ self.msgdata = msgdata
+ self.chain = chain
+
+
+class AcceptEvent(ChainEvent):
+ """A notification event signaling that a message is being accepted."""
+
+
+class AcceptOwnerEvent(ChainEvent):
+ """An event signaling that a message is accepted to the -owner address."""
+
+
+class DiscardEvent(ChainEvent):
+ """A notification event signaling that a message is being discarded."""
+
+
+class HoldEvent(ChainEvent):
+ """A notification event signaling that a message is being held."""
+
+
+class RejectEvent(ChainEvent):
+ """A notification event signaling that a message is being rejected."""
+
+
+
class LinkAction(Enum):
# Jump to another chain.
jump = 0
diff --git a/src/mailman/interfaces/configuration.py b/src/mailman/interfaces/configuration.py
new file mode 100644
index 000000000..8c4fb52a6
--- /dev/null
+++ b/src/mailman/interfaces/configuration.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Configuration system interface."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ConfigurationUpdatedEvent',
+ 'IConfiguration',
+ ]
+
+
+from zope.interface import Interface
+
+
+
+class IConfiguration(Interface):
+ """Marker interface; used for adaptation in the REST API."""
+
+
+
+class ConfigurationUpdatedEvent:
+ """The system-wide global configuration was updated."""
+ def __init__(self, config):
+ self.config = config
diff --git a/src/mailman/interfaces/database.py b/src/mailman/interfaces/database.py
index 0530f83b9..1f39daee7 100644
--- a/src/mailman/interfaces/database.py
+++ b/src/mailman/interfaces/database.py
@@ -17,16 +17,18 @@
"""Interfaces for database interaction."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'DatabaseError',
'IDatabase',
+ 'IDatabaseFactory',
+ 'ITemporaryDatabase',
]
-from zope.interface import Interface
+from zope.interface import Attribute, Interface
from mailman.interfaces.errors import MailmanError
@@ -49,12 +51,6 @@ class IDatabase(Interface):
configuration file setting.
"""
- def _reset():
- """Reset the database to its pristine state.
-
- This is only used by the test framework.
- """
-
def begin():
"""Begin the current transaction."""
@@ -63,3 +59,25 @@ class IDatabase(Interface):
def abort():
"""Abort the current transaction."""
+
+ store = Attribute(
+ """The underlying Storm store on which you can do queries.""")
+
+
+
+class ITemporaryDatabase(Interface):
+ """Marker interface for test suite adaptation."""
+
+
+
+class IDatabaseFactory(Interface):
+ "Interface for creating new databases."""
+
+ def create():
+ """Return a new `IDatabase`.
+
+ The database will be initialized and all migrations will be loaded.
+
+ :return: A new database.
+ :rtype: IDatabase
+ """
diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py
index 6f43edf3f..573ba11df 100644
--- a/src/mailman/interfaces/listmanager.py
+++ b/src/mailman/interfaces/listmanager.py
@@ -113,6 +113,15 @@ class IListManager(Interface):
not exist.
"""
+ def get_by_list_id(list_id):
+ """Return the mailing list with the given list id, if it exists.
+
+ :type fqdn_listname: Unicode.
+ :param fqdn_listname: The fully qualified name of the mailing list.
+ :return: the matching `IMailingList` or None if the named list does
+ not exist.
+ """
+
def delete(mlist):
"""Remove the mailing list from the database.
@@ -134,6 +143,10 @@ class IListManager(Interface):
"""An iterator over the fully qualified list names of all mailing
lists managed by this list manager.""")
+ list_ids = Attribute(
+ """An iterator over the list ids of all mailing lists managed by this
+ list manager.""")
+
name_components = Attribute(
"""An iterator over the 2-tuple of (list_name, mail_host) for all
mailing lists managed by this list manager.""")
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index bced070d3..8a4436a21 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -17,7 +17,7 @@
"""Interface for a mailing list."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -81,6 +81,11 @@ class IMailingList(Interface):
mail_host is 'example.com'.
""")
+ list_id = Attribute("""\
+ The identity of the mailing list. This value will never change. It
+ is defined in RFC 2369.
+ """)
+
fqdn_listname = Attribute("""\
The read-only fully qualified name of the mailing list. This is the
guaranteed unique id for the mailing list, and it is always the
@@ -103,9 +108,13 @@ class IMailingList(Interface):
mailing lists, or in headers, and so forth. It should be as succinct
as you can get it, while still identifying what the list is.""")
- include_list_post_header = Attribute(
- """Flag specifying whether to include the RFC 2369 List-Post header.
- This is usually set to True, except for announce-only lists.""")
+ allow_list_posts = Attribute(
+ """Flag specifying posts to the list are generally allowed.
+
+ This controls the value of the RFC 2369 List-Post header. This is
+ usually set to True, except for announce-only lists. When False, the
+ List-Post is set to NO as per the RFC.
+ """)
include_rfc2369_headers = Attribute(
"""Flag specifying whether to include any RFC 2369 header, including
@@ -250,6 +259,13 @@ class IMailingList(Interface):
# Delivery.
+ archive_policy = Attribute(
+ """The policy for archiving messages to this mailing list.
+
+ The value is an `ArchivePolicy` enum. Use this to archive the mailing
+ list publicly, privately, or not at all.
+ """)
+
last_post_at = Attribute(
"""The date and time a message was last posted to the mailing list.""")
@@ -511,6 +527,9 @@ class IMailingList(Interface):
without any other checks.
""")
+ newsgroup_moderation = Attribute(
+ """The moderation policy for the linked newsgroup, if there is one.""")
+
# Bounces.
forward_unrecognized_bounces_to = Attribute(
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py
index 52bacc72d..997338835 100644
--- a/src/mailman/interfaces/member.py
+++ b/src/mailman/interfaces/member.py
@@ -136,8 +136,11 @@ class IMember(Interface):
member_id = Attribute(
"""The member's unique, random identifier as a UUID.""")
+ list_id = Attribute(
+ """The list id of the mailing list the member is subscribed to.""")
+
mailing_list = Attribute(
- """The mailing list subscribed to.""")
+ """The `IMailingList` that the member is subscribed to.""")
address = Attribute(
"""The email address that's subscribed to the list.""")
diff --git a/src/mailman/interfaces/nntp.py b/src/mailman/interfaces/nntp.py
index d5d08d3f0..22b8b1754 100644
--- a/src/mailman/interfaces/nntp.py
+++ b/src/mailman/interfaces/nntp.py
@@ -15,9 +15,13 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""NNTP and newsgroup interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
__metaclass__ = type
__all__ = [
- 'NewsModeration',
+ 'NewsgroupModeration',
]
@@ -25,7 +29,7 @@ from flufl.enum import Enum
-class NewsModeration(Enum):
+class NewsgroupModeration(Enum):
# The newsgroup is not moderated.
none = 0
# The newsgroup is moderated, but allows for an open posting policy.
diff --git a/src/mailman/interfaces/runner.py b/src/mailman/interfaces/runner.py
index 4611fa3a7..9a3c9baa4 100644
--- a/src/mailman/interfaces/runner.py
+++ b/src/mailman/interfaces/runner.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'IRunner',
+ 'RunnerCrashEvent',
]
@@ -29,6 +30,18 @@ from zope.interface import Interface, Attribute
+class RunnerCrashEvent:
+ """Triggered when a runner encounters an exception in _dispose()."""
+
+ def __init__(self, runner, mlist, msg, metadata, error):
+ self.runner = runner
+ self.mailing_list = mlist
+ self.message = msg
+ self.metadata = metadata
+ self.error = error
+
+
+
class IRunner(Interface):
"""The runner."""
diff --git a/src/mailman/interfaces/subscriptions.py b/src/mailman/interfaces/subscriptions.py
index 85f333cf8..cb4900053 100644
--- a/src/mailman/interfaces/subscriptions.py
+++ b/src/mailman/interfaces/subscriptions.py
@@ -69,7 +69,7 @@ class ISubscriptionService(Interface):
:rtype: `IMember`
"""
- def find_members(subscriber=None, fqdn_listname=None, role=None):
+ def find_members(subscriber=None, list_id=None, role=None):
"""Search for and return a specific member.
The members are sorted first by fully-qualified mailing list name,
@@ -80,9 +80,9 @@ class ISubscriptionService(Interface):
:param subscriber: The email address or user id of the user getting
subscribed.
:type subscriber: string or int
- :param fqdn_listname: The posting address of the mailing list to
- search for the subscriber's memberships on.
- :type fqdn_listname: string
+ :param list_id: The list id of the mailing list to search for the
+ subscriber's memberships on.
+ :type list_id: string
:param role: The member role.
:type role: `MemberRole`
:return: The list of all memberships, which may be empty.
@@ -92,8 +92,8 @@ class ISubscriptionService(Interface):
def __iter__():
"""See `get_members()`."""
- def join(fqdn_listname, subscriber, display_name=None,
- delivery_mode=DeliveryMode.regular,
+ def join(list_id, subscriber, display_name=None,
+ delivery_mode=DeliveryMode.regular,
role=MemberRole.member):
"""Subscribe to a mailing list.
@@ -103,9 +103,9 @@ class ISubscriptionService(Interface):
the subscription request is still dependent on the policy of the
mailing list.
- :param fqdn_listname: The posting address of the mailing list to
- subscribe the user to.
- :type fqdn_listname: string
+ :param list_id: The list id of the mailing list the user is
+ subscribing to.
+ :type list_id: string
:param subscriber: The email address or user id of the user getting
subscribed.
:type subscriber: string or int
@@ -130,12 +130,12 @@ class ISubscriptionService(Interface):
:raises ValueError: when `delivery_mode` is invalid.
"""
- def leave(fqdn_listname, email):
+ def leave(list_id, email):
"""Unsubscribe from a mailing list.
- :param fqdn_listname: The posting address of the mailing list to
- unsubscribe the user from.
- :type fqdn_listname: string
+ :param list_id: The list id of the mailing list the user is
+ unsubscribing from.
+ :type list_id: string
:param email: The email address of the user getting unsubscribed.
:type email: string
:raises InvalidEmailAddressError: if the email address is not valid.
diff --git a/src/mailman/languages/language.py b/src/mailman/languages/language.py
index 04f9c6639..effcf9c1b 100644
--- a/src/mailman/languages/language.py
+++ b/src/mailman/languages/language.py
@@ -18,7 +18,7 @@
"""The representation of a language."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,16 +26,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
+
from mailman.interfaces.languages import ILanguage
+@implementer(ILanguage)
class Language:
"""The representation of a language."""
- implements(ILanguage)
-
def __init__(self, code, charset, description):
self.code = code
self.charset = charset
diff --git a/src/mailman/languages/manager.py b/src/mailman/languages/manager.py
index ca7001102..87b56dbda 100644
--- a/src/mailman/languages/manager.py
+++ b/src/mailman/languages/manager.py
@@ -17,25 +17,27 @@
"""Language manager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'LanguageManager',
]
-from zope.interface import implements
+from zope.component import getUtility
+from zope.interface import implementer
+
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.languages import ILanguageManager
from mailman.languages.language import Language
+@implementer(ILanguageManager)
class LanguageManager:
"""Language manager."""
- implements(ILanguageManager)
-
def __init__(self):
# Mapping from 2-letter code to Language instance.
self._languages = {}
@@ -73,3 +75,18 @@ class LanguageManager:
def clear(self):
"""See `ILanguageManager`."""
self._languages.clear()
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if not isinstance(event, ConfigurationUpdatedEvent):
+ return
+ manager = getUtility(ILanguageManager)
+ for language in event.config.language_configs:
+ if language.enabled:
+ code = language.name.split('.')[1]
+ manager.add(code, language.charset, language.description)
+ # the default language must always be available.
+ assert event.config.mailman.default_language in manager, (
+ 'system default language code not defined: {0}'.format(
+ event.config.mailman.default_language))
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index a12a993a8..d8ab65a80 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -17,7 +17,7 @@
"""Model for addresses."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
from email.utils import formataddr
from storm.locals import DateTime, Int, Reference, Unicode
from zope.event import notify
-from zope.interface import implements
+from zope.interface import implementer
from mailman.database.model import Model
from mailman.interfaces.address import AddressVerificationEvent, IAddress
@@ -36,8 +36,9 @@ from mailman.utilities.datetime import now
+@implementer(IAddress)
class Address(Model):
- implements(IAddress)
+ """See `IAddress`."""
id = Int(primary=True)
email = Unicode()
diff --git a/src/mailman/model/autorespond.py b/src/mailman/model/autorespond.py
index 7b42205b4..567dcd19e 100644
--- a/src/mailman/model/autorespond.py
+++ b/src/mailman/model/autorespond.py
@@ -17,7 +17,7 @@
"""Module stuff."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,10 +27,10 @@ __all__ = [
from storm.locals import And, Date, Desc, Int, Reference
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.autorespond import (
IAutoResponseRecord, IAutoResponseSet, Response)
@@ -38,8 +38,9 @@ from mailman.utilities.datetime import today
+@implementer(IAutoResponseRecord)
class AutoResponseRecord(Model):
- implements(IAutoResponseRecord)
+ """See `IAutoResponseRecord`."""
id = Int(primary=True)
@@ -60,33 +61,37 @@ class AutoResponseRecord(Model):
+@implementer(IAutoResponseSet)
class AutoResponseSet:
- implements(IAutoResponseSet)
+ """See `IAutoResponseSet`."""
def __init__(self, mailing_list):
self._mailing_list = mailing_list
- def todays_count(self, address, response_type):
+ @dbconnection
+ def todays_count(self, store, address, response_type):
"""See `IAutoResponseSet`."""
- return config.db.store.find(
+ return store.find(
AutoResponseRecord,
And(AutoResponseRecord.address == address,
AutoResponseRecord.mailing_list == self._mailing_list,
AutoResponseRecord.response_type == response_type,
AutoResponseRecord.date_sent == today())).count()
- def response_sent(self, address, response_type):
+ @dbconnection
+ def response_sent(self, store, address, response_type):
"""See `IAutoResponseSet`."""
response = AutoResponseRecord(
self._mailing_list, address, response_type)
- config.db.store.add(response)
+ store.add(response)
- def last_response(self, address, response_type):
+ @dbconnection
+ def last_response(self, store, address, response_type):
"""See `IAutoResponseSet`."""
- results = config.db.store.find(
+ results = store.find(
AutoResponseRecord,
And(AutoResponseRecord.address == address,
AutoResponseRecord.mailing_list == self._mailing_list,
AutoResponseRecord.response_type == response_type)
- ).order_by(Desc(AutoResponseRecord.date_sent))
+ ).order_by(Desc(AutoResponseRecord.date_sent))
return (None if results.count() == 0 else results.first())
diff --git a/src/mailman/model/bans.py b/src/mailman/model/bans.py
index 9dc0c51ba..b6de9336f 100644
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -17,7 +17,7 @@
"""Ban manager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,16 +28,17 @@ __all__ = [
import re
from storm.locals import Int, Unicode
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.interfaces.bans import IBan, IBanManager
+@implementer(IBan)
class Ban(Model):
- implements(IBan)
+ """See `IBan`."""
id = Int(primary=True)
email = Unicode()
@@ -50,46 +51,47 @@ class Ban(Model):
+@implementer(IBanManager)
class BanManager:
- implements(IBanManager)
+ """See `IBanManager`."""
- def ban(self, email, mailing_list=None):
+ @dbconnection
+ def ban(self, store, email, mailing_list=None):
"""See `IBanManager`."""
- bans = config.db.store.find(
- Ban, email=email, mailing_list=mailing_list)
+ bans = store.find(Ban, email=email, mailing_list=mailing_list)
if bans.count() == 0:
ban = Ban(email, mailing_list)
- config.db.store.add(ban)
+ store.add(ban)
- def unban(self, email, mailing_list=None):
+ @dbconnection
+ def unban(self, store, email, mailing_list=None):
"""See `IBanManager`."""
- ban = config.db.store.find(
- Ban, email=email, mailing_list=mailing_list).one()
+ ban = store.find(Ban, email=email, mailing_list=mailing_list).one()
if ban is not None:
- config.db.store.remove(ban)
+ store.remove(ban)
- def is_banned(self, email, mailing_list=None):
+ @dbconnection
+ def is_banned(self, store, email, mailing_list=None):
"""See `IBanManager`."""
# A specific mailing list ban is being checked, however the email
# address could be banned specifically, or globally.
if mailing_list is not None:
# Try specific bans first.
- bans = config.db.store.find(
- Ban, email=email, mailing_list=mailing_list)
+ bans = store.find(Ban, email=email, mailing_list=mailing_list)
if bans.count() > 0:
return True
# Try global bans next.
- bans = config.db.store.find(Ban, email=email, mailing_list=None)
+ bans = store.find(Ban, email=email, mailing_list=None)
if bans.count() > 0:
return True
# Now try specific mailing list bans, but with a pattern.
- bans = config.db.store.find(Ban, mailing_list=mailing_list)
+ bans = store.find(Ban, mailing_list=mailing_list)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
return True
# And now try global pattern bans.
- bans = config.db.store.find(Ban, mailing_list=None)
+ bans = store.find(Ban, mailing_list=None)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
@@ -97,12 +99,11 @@ class BanManager:
else:
# The client is asking for global bans. Look up bans on the
# specific email address first.
- bans = config.db.store.find(
- Ban, email=email, mailing_list=None)
+ bans = store.find(Ban, email=email, mailing_list=None)
if bans.count() > 0:
return True
# And now look for global pattern bans.
- bans = config.db.store.find(Ban, mailing_list=None)
+ bans = store.find(Ban, mailing_list=None)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 8c55e3d16..628e076bf 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -17,7 +17,7 @@
"""Bounce support."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,10 +27,10 @@ __all__ = [
from storm.locals import Bool, Int, DateTime, Unicode
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.bounce import (
BounceContext, IBounceEvent, IBounceProcessor)
@@ -38,8 +38,9 @@ from mailman.utilities.datetime import now
+@implementer(IBounceEvent)
class BounceEvent(Model):
- implements(IBounceEvent)
+ """See `IBounceEvent`."""
id = Int(primary=True)
list_name = Unicode()
@@ -59,24 +60,27 @@ class BounceEvent(Model):
+@implementer(IBounceProcessor)
class BounceProcessor:
- implements(IBounceProcessor)
+ """See `IBounceProcessor`."""
- def register(self, mlist, email, msg, where=None):
+ @dbconnection
+ def register(self, store, mlist, email, msg, where=None):
"""See `IBounceProcessor`."""
event = BounceEvent(mlist.fqdn_listname, email, msg, where)
- config.db.store.add(event)
+ store.add(event)
return event
@property
- def events(self):
+ @dbconnection
+ def events(self, store):
"""See `IBounceProcessor`."""
- for event in config.db.store.find(BounceEvent):
+ for event in store.find(BounceEvent):
yield event
@property
- def unprocessed(self):
+ @dbconnection
+ def unprocessed(self, store):
"""See `IBounceProcessor`."""
- for event in config.db.store.find(BounceEvent,
- BounceEvent.processed == False):
+ for event in store.find(BounceEvent, BounceEvent.processed == False):
yield event
diff --git a/src/mailman/model/digests.py b/src/mailman/model/digests.py
index d7805ebf6..1d422ce8b 100644
--- a/src/mailman/model/digests.py
+++ b/src/mailman/model/digests.py
@@ -17,7 +17,7 @@
"""One last digest."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,7 +26,7 @@ __all__ = [
from storm.locals import Int, Reference
-from zope.interface import implements
+from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.types import Enum
@@ -35,8 +35,9 @@ from mailman.interfaces.member import DeliveryMode
+@implementer(IOneLastDigest)
class OneLastDigest(Model):
- implements(IOneLastDigest)
+ """See `IOneLastDigest`."""
id = Int(primary=True)
diff --git a/src/mailman/model/docs/listmanager.rst b/src/mailman/model/docs/listmanager.rst
index 9c72b18e7..380fe7704 100644
--- a/src/mailman/model/docs/listmanager.rst
+++ b/src/mailman/model/docs/listmanager.rst
@@ -16,28 +16,31 @@ Creating a mailing list
Creating the list returns the newly created IMailList object.
>>> from mailman.interfaces.mailinglist import IMailingList
- >>> mlist = list_manager.create('_xtest@example.com')
+ >>> mlist = list_manager.create('test@example.com')
>>> IMailingList.providedBy(mlist)
True
-All lists with identities have a short name, a host name, and a fully
-qualified listname. This latter is what uniquely distinguishes the mailing
-list to the system.
+All lists with identities have a short name, a host name, a fully qualified
+listname, and an `RFC 2369`_ list id. This latter will not change even if the
+mailing list moves to a different host, so it is what uniquely distinguishes
+the mailing list to the system.
>>> print mlist.list_name
- _xtest
+ test
>>> print mlist.mail_host
example.com
>>> print mlist.fqdn_listname
- _xtest@example.com
+ test@example.com
+ >>> print mlist.list_id
+ test.example.com
If you try to create a mailing list with the same name as an existing list,
you will get an exception.
- >>> list_manager.create('_xtest@example.com')
+ >>> list_manager.create('test@example.com')
Traceback (most recent call last):
...
- ListAlreadyExistsError: _xtest@example.com
+ ListAlreadyExistsError: test@example.com
It is an error to create a mailing list that isn't a fully qualified list name
(i.e. posting address).
@@ -59,9 +62,9 @@ Use the list manager to delete a mailing list.
After deleting the list, you can create it again.
- >>> mlist = list_manager.create('_xtest@example.com')
+ >>> mlist = list_manager.create('test@example.com')
>>> print mlist.fqdn_listname
- _xtest@example.com
+ test@example.com
Retrieving a mailing list
@@ -70,13 +73,21 @@ Retrieving a mailing list
When a mailing list exists, you can ask the list manager for it and you will
always get the same object back.
- >>> mlist_2 = list_manager.get('_xtest@example.com')
+ >>> mlist_2 = list_manager.get('test@example.com')
+ >>> mlist_2 is mlist
+ True
+
+You can also get a mailing list by it's list id.
+
+ >>> mlist_2 = list_manager.get_by_list_id('test.example.com')
>>> mlist_2 is mlist
True
If you try to get a list that doesn't existing yet, you get ``None``.
- >>> print list_manager.get('_xtest_2@example.com')
+ >>> print list_manager.get('test_2@example.com')
+ None
+ >>> print list_manager.get_by_list_id('test_2.example.com')
None
You also get ``None`` if the list name is invalid.
@@ -93,25 +104,34 @@ iterate over the mailing list objects, the list posting addresses, or the list
address components.
::
- >>> mlist_3 = list_manager.create('_xtest_3@example.com')
- >>> mlist_4 = list_manager.create('_xtest_4@example.com')
+ >>> mlist_3 = list_manager.create('test_3@example.com')
+ >>> mlist_4 = list_manager.create('test_4@example.com')
>>> for name in sorted(list_manager.names):
... print name
- _xtest@example.com
- _xtest_3@example.com
- _xtest_4@example.com
+ test@example.com
+ test_3@example.com
+ test_4@example.com
+
+ >>> for list_id in sorted(list_manager.list_ids):
+ ... print list_id
+ test.example.com
+ test_3.example.com
+ test_4.example.com
>>> for fqdn_listname in sorted(m.fqdn_listname
... for m in list_manager.mailing_lists):
... print fqdn_listname
- _xtest@example.com
- _xtest_3@example.com
- _xtest_4@example.com
+ test@example.com
+ test_3@example.com
+ test_4@example.com
>>> for list_name, mail_host in sorted(list_manager.name_components,
... key=lambda (name, host): name):
... print list_name, '@', mail_host
- _xtest @ example.com
- _xtest_3 @ example.com
- _xtest_4 @ example.com
+ test @ example.com
+ test_3 @ example.com
+ test_4 @ example.com
+
+
+.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html
diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst
index 895068e52..21c2f0fd8 100644
--- a/src/mailman/model/docs/mailinglist.rst
+++ b/src/mailman/model/docs/mailinglist.rst
@@ -5,11 +5,13 @@ Mailing lists
.. XXX 2010-06-18 BAW: This documentation needs a lot more detail.
The mailing list is a core object in Mailman. It is uniquely identified in
-the system by its posting address, i.e. the email address you would send a
-message to in order to post a message to the mailing list. This must be fully
-qualified.
+the system by its *list-id* which is derived from its posting address,
+i.e. the email address you would send a message to in order to post a message
+to the mailing list. The list id is defined in `RFC 2369`_.
>>> mlist = create_list('aardvark@example.com')
+ >>> print mlist.list_id
+ aardvark.example.com
>>> print mlist.fqdn_listname
aardvark@example.com
@@ -163,3 +165,6 @@ A user cannot subscribe to a mailing list without a preferred address.
...
MissingPreferredAddressError: User must have a preferred address:
<User "Elly Person" (2) at ...>
+
+
+.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html
diff --git a/src/mailman/model/docs/membership.rst b/src/mailman/model/docs/membership.rst
index f070b4d40..3286bfe6e 100644
--- a/src/mailman/model/docs/membership.rst
+++ b/src/mailman/model/docs/membership.rst
@@ -283,8 +283,8 @@ though that the address their changing to must be verified.
>>> gwen_address = list(gwen.addresses)[0]
>>> gwen_member = bee.subscribe(gwen_address)
>>> for m in bee.members.members:
- ... print m.member_id.int, m.mailing_list, m.address.email
- 7 bee@example.com gwen@example.com
+ ... print m.member_id.int, m.mailing_list.list_id, m.address.email
+ 7 bee.example.com gwen@example.com
Gwen gets a email address.
@@ -301,8 +301,8 @@ address, but the address is not yet verified.
Her membership has not changed.
>>> for m in bee.members.members:
- ... print m.member_id.int, m.mailing_list, m.address.email
- 7 bee@example.com gwen@example.com
+ ... print m.member_id.int, m.mailing_list.list_id, m.address.email
+ 7 bee.example.com gwen@example.com
Gwen verifies her email address, and updates her membership.
@@ -313,5 +313,5 @@ Gwen verifies her email address, and updates her membership.
Now her membership reflects the new address.
>>> for m in bee.members.members:
- ... print m.member_id.int, m.mailing_list, m.address.email
- 7 bee@example.com gperson@example.com
+ ... print m.member_id.int, m.mailing_list.list_id, m.address.email
+ 7 bee.example.com gperson@example.com
diff --git a/src/mailman/model/docs/pending.rst b/src/mailman/model/docs/pending.rst
index 1bf1ee0e9..3d33dd5da 100644
--- a/src/mailman/model/docs/pending.rst
+++ b/src/mailman/model/docs/pending.rst
@@ -15,11 +15,14 @@ In order to pend an event, you first need a pending database.
The pending database can add any ``IPendable`` to the database, returning a
token that can be used in urls and such.
+::
- >>> from zope.interface import implements
+ >>> from zope.interface import implementer
>>> from mailman.interfaces.pending import IPendable
- >>> class SimplePendable(dict):
- ... implements(IPendable)
+ >>> @implementer(IPendable)
+ ... class SimplePendable(dict):
+ ... pass
+
>>> subscription = SimplePendable(
... type='subscription',
... address='aperson@example.com',
diff --git a/src/mailman/model/docs/registration.rst b/src/mailman/model/docs/registration.rst
index eecb3a8cd..58e9d7a86 100644
--- a/src/mailman/model/docs/registration.rst
+++ b/src/mailman/model/docs/registration.rst
@@ -318,12 +318,15 @@ confirm method will just return False.
Likewise, if you try to confirm, through the `IUserRegistrar` interface, a
token that doesn't match a registration event, you will get ``None``.
However, the pending event matched with that token will still be removed.
+::
>>> from mailman.interfaces.pending import IPendable
- >>> from zope.interface import implements
+ >>> from zope.interface import implementer
+
+ >>> @implementer(IPendable)
+ ... class SimplePendable(dict):
+ ... pass
- >>> class SimplePendable(dict):
- ... implements(IPendable)
>>> pendable = SimplePendable(type='foo', bar='baz')
>>> token = pendingdb.add(pendable)
>>> registrar.confirm(token)
diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst
index a20823a91..a51cbc099 100644
--- a/src/mailman/model/docs/requests.rst
+++ b/src/mailman/model/docs/requests.rst
@@ -696,7 +696,7 @@ Frank Person is now a member of the mailing list.
>>> print member.user.display_name
Frank Person
>>> print member.user.password
- {CLEARTEXT}abcxyz
+ {plaintext}abcxyz
Holding unsubscription requests
diff --git a/src/mailman/model/docs/usermanager.rst b/src/mailman/model/docs/usermanager.rst
index 727f82835..cf7672b27 100644
--- a/src/mailman/model/docs/usermanager.rst
+++ b/src/mailman/model/docs/usermanager.rst
@@ -173,8 +173,9 @@ There are now four members in the system. Sort them by address then role.
... return (member.address.email, member.role.name)
>>> members = sorted(user_manager.members, key=sort_key)
>>> for member in members:
- ... print member.mailing_list, member.address.email, member.role
- test@example.com bperson@example.com MemberRole.member
- test@example.com bperson@example.com MemberRole.owner
- test@example.com eperson@example.com MemberRole.member
- test@example.com fperson@example.com MemberRole.member
+ ... print member.mailing_list.list_id, member.address.email, \
+ ... member.role
+ test.example.com bperson@example.com MemberRole.member
+ test.example.com bperson@example.com MemberRole.owner
+ test.example.com eperson@example.com MemberRole.member
+ test.example.com fperson@example.com MemberRole.member
diff --git a/src/mailman/model/docs/users.rst b/src/mailman/model/docs/users.rst
index 95e08a8d7..997f983b2 100644
--- a/src/mailman/model/docs/users.rst
+++ b/src/mailman/model/docs/users.rst
@@ -335,11 +335,12 @@ membership role.
... return (member.address.email, member.mailing_list,
... int(member.role))
>>> for member in sorted(members, key=sortkey):
- ... print member.address.email, member.mailing_list, member.role
- zperson@example.com xtest_1@example.com MemberRole.member
- zperson@example.net xtest_3@example.com MemberRole.moderator
- zperson@example.org xtest_2@example.com MemberRole.member
- zperson@example.org xtest_2@example.com MemberRole.owner
+ ... print member.address.email, member.mailing_list.list_id, \
+ ... member.role
+ zperson@example.com xtest_1.example.com MemberRole.member
+ zperson@example.net xtest_3.example.com MemberRole.moderator
+ zperson@example.org xtest_2.example.com MemberRole.member
+ zperson@example.org xtest_2.example.com MemberRole.owner
.. _`usermanager.txt`: usermanager.html
diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py
index 49c935740..de6a9005a 100644
--- a/src/mailman/model/domain.py
+++ b/src/mailman/model/domain.py
@@ -17,7 +17,7 @@
"""Domains."""
-from __future__ import unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -29,10 +29,10 @@ __all__ = [
from urlparse import urljoin, urlparse
from storm.locals import Int, Unicode
from zope.event import notify
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.interfaces.domain import (
BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent,
DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager)
@@ -40,11 +40,10 @@ from mailman.model.mailinglist import MailingList
+@implementer(IDomain)
class Domain(Model):
"""Domains."""
- implements(IDomain)
-
id = Int(primary=True)
mail_host = Unicode()
@@ -90,9 +89,10 @@ class Domain(Model):
return urlparse(self.base_url).scheme
@property
- def mailing_lists(self):
+ @dbconnection
+ def mailing_lists(self, store):
"""See `IDomain`."""
- mailing_lists = config.db.store.find(
+ mailing_lists = store.find(
MailingList,
MailingList.mail_host == self.mail_host)
for mlist in mailing_lists:
@@ -114,12 +114,13 @@ class Domain(Model):
+@implementer(IDomainManager)
class DomainManager:
"""Domain manager."""
- implements(IDomainManager)
-
- def add(self, mail_host,
+ @dbconnection
+ def add(self, store,
+ mail_host,
description=None,
base_url=None,
contact_address=None):
@@ -131,20 +132,22 @@ class DomainManager:
'Duplicate email host: %s' % mail_host)
notify(DomainCreatingEvent(mail_host))
domain = Domain(mail_host, description, base_url, contact_address)
- config.db.store.add(domain)
+ store.add(domain)
notify(DomainCreatedEvent(domain))
return domain
- def remove(self, mail_host):
+ @dbconnection
+ def remove(self, store, mail_host):
domain = self[mail_host]
notify(DomainDeletingEvent(domain))
- config.db.store.remove(domain)
+ store.remove(domain)
notify(DomainDeletedEvent(mail_host))
return domain
- def get(self, mail_host, default=None):
+ @dbconnection
+ def get(self, store, mail_host, default=None):
"""See `IDomainManager`."""
- domains = config.db.store.find(Domain, mail_host=mail_host)
+ domains = store.find(Domain, mail_host=mail_host)
if domains.count() < 1:
return default
assert domains.count() == 1, (
@@ -159,14 +162,17 @@ class DomainManager:
raise KeyError(mail_host)
return domain
- def __len__(self):
- return config.db.store.find(Domain).count()
+ @dbconnection
+ def __len__(self, store):
+ return store.find(Domain).count()
- def __iter__(self):
+ @dbconnection
+ def __iter__(self, store):
"""See `IDomainManager`."""
- for domain in config.db.store.find(Domain):
+ for domain in store.find(Domain):
yield domain
- def __contains__(self, mail_host):
+ @dbconnection
+ def __contains__(self, store, mail_host):
"""See `IDomainManager`."""
- return config.db.store.find(Domain, mail_host=mail_host).count() > 0
+ return store.find(Domain, mail_host=mail_host).count() > 0
diff --git a/src/mailman/model/language.py b/src/mailman/model/language.py
index da86b326c..b593721df 100644
--- a/src/mailman/model/language.py
+++ b/src/mailman/model/language.py
@@ -17,7 +17,7 @@
"""Model for languages."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,15 +26,16 @@ __all__ = [
from storm.locals import Int, Unicode
-from zope.interface import implements
+from zope.interface import implementer
from mailman.database import Model
from mailman.interfaces import ILanguage
+@implementer(ILanguage)
class Language(Model):
- implements(ILanguage)
+ """See `ILanguage`."""
id = Int(primary=True)
code = Unicode()
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index 0ea87a082..ce94047dd 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -17,7 +17,7 @@
"""A mailing list manager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,9 +26,9 @@ __all__ = [
from zope.event import notify
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
+from mailman.database.transaction import dbconnection
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import (
IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,
@@ -38,66 +38,84 @@ from mailman.utilities.datetime import now
+@implementer(IListManager)
class ListManager:
"""An implementation of the `IListManager` interface."""
- implements(IListManager)
-
- def create(self, fqdn_listname):
+ @dbconnection
+ def create(self, store, fqdn_listname):
"""See `IListManager`."""
listname, at, hostname = fqdn_listname.partition('@')
if len(hostname) == 0:
raise InvalidEmailAddressError(fqdn_listname)
+ list_id = '{0}.{1}'.format(listname, hostname)
notify(ListCreatingEvent(fqdn_listname))
- mlist = config.db.store.find(
+ mlist = store.find(
MailingList,
- MailingList.list_name == listname,
- MailingList.mail_host == hostname).one()
+ MailingList._list_id == list_id).one()
if mlist:
raise ListAlreadyExistsError(fqdn_listname)
mlist = MailingList(fqdn_listname)
mlist.created_at = now()
- config.db.store.add(mlist)
+ store.add(mlist)
notify(ListCreatedEvent(mlist))
return mlist
- def get(self, fqdn_listname):
+ @dbconnection
+ def get(self, store, fqdn_listname):
"""See `IListManager`."""
listname, at, hostname = fqdn_listname.partition('@')
- return config.db.store.find(MailingList,
- list_name=listname,
- mail_host=hostname).one()
+ list_id = '{0}.{1}'.format(listname, hostname)
+ return store.find(MailingList, MailingList._list_id == list_id).one()
+
+ @dbconnection
+ def get_by_list_id(self, store, list_id):
+ """See `IListManager`."""
+ return store.find(MailingList, MailingList._list_id == list_id).one()
- def delete(self, mlist):
+ @dbconnection
+ def delete(self, store, mlist):
"""See `IListManager`."""
fqdn_listname = mlist.fqdn_listname
notify(ListDeletingEvent(mlist))
- config.db.store.remove(mlist)
+ store.remove(mlist)
notify(ListDeletedEvent(fqdn_listname))
@property
- def mailing_lists(self):
+ @dbconnection
+ def mailing_lists(self, store):
"""See `IListManager`."""
- for mlist in config.db.store.find(MailingList):
+ for mlist in store.find(MailingList):
yield mlist
- def __iter__(self):
+ @dbconnection
+ def __iter__(self, store):
"""See `IListManager`."""
- for mlist in config.db.store.find(MailingList):
+ for mlist in store.find(MailingList):
yield mlist
@property
- def names(self):
+ @dbconnection
+ def names(self, store):
"""See `IListManager`."""
- result_set = config.db.store.find(MailingList)
- for mail_host, list_name in result_set.values(MailingList.mail_host,
+ result_set = store.find(MailingList)
+ for mail_host, list_name in result_set.values(MailingList.mail_host,
MailingList.list_name):
yield '{0}@{1}'.format(list_name, mail_host)
@property
- def name_components(self):
+ @dbconnection
+ def list_ids(self, store):
+ """See `IListManager`."""
+ result_set = store.find(MailingList)
+ for list_id in result_set.values(MailingList._list_id):
+ yield list_id
+
+ @property
+ @dbconnection
+ def name_components(self, store):
"""See `IListManager`."""
- result_set = config.db.store.find(MailingList)
- for mail_host, list_name in result_set.values(MailingList.mail_host,
+ result_set = store.find(MailingList)
+ for mail_host, list_name in result_set.values(MailingList.mail_host,
MailingList.list_name):
yield list_name, mail_host
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index d51c89514..2c55540be 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -17,7 +17,7 @@
"""Model for mailing lists."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -33,13 +33,14 @@ from storm.locals import (
TimeDelta, Unicode)
from urlparse import urljoin
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.action import Action, FilterAction
from mailman.interfaces.address import IAddress
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.digests import DigestFrequency
@@ -51,7 +52,7 @@ from mailman.interfaces.mailinglist import (
from mailman.interfaces.member import (
AlreadySubscribedError, MemberRole, MissingPreferredAddressError)
from mailman.interfaces.mime import FilterType
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.user import IUser
from mailman.model import roster
from mailman.model.digests import OneLastDigest
@@ -67,8 +68,9 @@ UNDERSCORE = '_'
+@implementer(IMailingList)
class MailingList(Model):
- implements(IMailingList)
+ """See `IMailingList`."""
id = Int(primary=True)
@@ -78,7 +80,8 @@ class MailingList(Model):
# List identity
list_name = Unicode()
mail_host = Unicode()
- include_list_post_header = Bool()
+ _list_id = Unicode(name='list_id')
+ allow_list_posts = Bool()
include_rfc2369_headers = Bool()
advertised = Bool()
anonymous_list = Bool()
@@ -103,9 +106,7 @@ class MailingList(Model):
admin_immed_notify = Bool()
admin_notify_mchanges = Bool()
administrivia = Bool()
- archive = Bool() # XXX
- archive_private = Bool() # XXX
- archive_volume_frequency = Int() # XXX
+ archive_policy = Enum(ArchivePolicy)
# Automatic responses.
autoresponse_grace_period = TimeDelta()
autorespond_owner = Enum(ResponseAction)
@@ -148,7 +149,6 @@ class MailingList(Model):
forward_auto_discards = Bool()
gateway_to_mail = Bool()
gateway_to_news = Bool()
- generic_nonmember_action = Int()
goodbye_message_uri = Unicode()
header_matches = Pickle()
header_uri = Unicode()
@@ -162,9 +162,8 @@ class MailingList(Model):
mime_is_default_digest = Bool()
moderator_password = RawStr()
new_member_options = Int()
- news_moderation = Enum(NewsModeration)
- news_prefix_subject_too = Bool()
- nntp_host = Unicode()
+ newsgroup_moderation = Enum(NewsgroupModeration)
+ nntp_prefix_subject_too = Bool()
nondigestable = Bool()
nonmember_rejection_notice = Unicode()
obscure_addresses = Bool()
@@ -201,6 +200,7 @@ class MailingList(Model):
assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
self.list_name = listname
self.mail_host = hostname
+ self._list_id = '{0}.{1}'.format(listname, hostname)
# For the pending database
self.next_request_id = 1
# We need to set up the rosters. Normally, this method will get
@@ -233,6 +233,11 @@ class MailingList(Model):
return '{0}@{1}'.format(self.list_name, self.mail_host)
@property
+ def list_id(self):
+ """See `IMailingList`."""
+ return self._list_id
+
+ @property
def domain(self):
"""See `IMailingList`."""
return getUtility(IDomainManager)[self.mail_host]
@@ -465,7 +470,7 @@ class MailingList(Model):
member = store.find(
Member,
Member.role == role,
- Member.mailing_list == self.fqdn_listname,
+ Member.list_id == self._list_id,
Member._address == subscriber).one()
if member:
raise AlreadySubscribedError(
@@ -476,7 +481,7 @@ class MailingList(Model):
member = store.find(
Member,
Member.role == role,
- Member.mailing_list == self.fqdn_listname,
+ Member.list_id == self._list_id,
Member._user == subscriber).one()
if member:
raise AlreadySubscribedError(
@@ -484,7 +489,7 @@ class MailingList(Model):
else:
raise ValueError('subscriber must be an address or user')
member = Member(role=role,
- mailing_list=self.fqdn_listname,
+ list_id=self._list_id,
subscriber=subscriber)
member.preferences = Preferences()
store.add(member)
@@ -492,8 +497,9 @@ class MailingList(Model):
+@implementer(IAcceptableAlias)
class AcceptableAlias(Model):
- implements(IAcceptableAlias)
+ """See `IAcceptableAlias`."""
id = Int(primary=True)
@@ -507,8 +513,10 @@ class AcceptableAlias(Model):
self.alias = alias
+
+@implementer(IAcceptableAliasSet)
class AcceptableAliasSet:
- implements(IAcceptableAliasSet)
+ """See `IAcceptableAliasSet`."""
def __init__(self, mailing_list):
self._mailing_list = mailing_list
diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py
index ae83fb388..76fe2f992 100644
--- a/src/mailman/model/member.py
+++ b/src/mailman/model/member.py
@@ -17,7 +17,7 @@
"""Model for members."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,11 +27,11 @@ __all__ = [
from storm.locals import Int, Reference, Unicode
from storm.properties import UUID
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.action import Action
from mailman.interfaces.address import IAddress
@@ -46,13 +46,14 @@ uid_factory = UniqueIDFactory(context='members')
+@implementer(IMember)
class Member(Model):
- implements(IMember)
+ """See `IMember`."""
id = Int(primary=True)
_member_id = UUID()
role = Enum(MemberRole)
- mailing_list = Unicode()
+ list_id = Unicode()
moderation_action = Enum(Action)
address_id = Int()
@@ -62,10 +63,10 @@ class Member(Model):
user_id = Int()
_user = Reference(user_id, 'User.id')
- def __init__(self, role, mailing_list, subscriber):
+ def __init__(self, role, list_id, subscriber):
self._member_id = uid_factory.new_uid()
self.role = role
- self.mailing_list = mailing_list
+ self.list_id = list_id
if IAddress.providedBy(subscriber):
self._address = subscriber
# Look this up dynamically.
@@ -79,17 +80,23 @@ class Member(Model):
if role in (MemberRole.owner, MemberRole.moderator):
self.moderation_action = Action.accept
elif role is MemberRole.member:
- self.moderation_action = getUtility(IListManager).get(
- mailing_list).default_member_action
+ self.moderation_action = getUtility(IListManager).get_by_list_id(
+ list_id).default_member_action
else:
assert role is MemberRole.nonmember, (
'Invalid MemberRole: {0}'.format(role))
- self.moderation_action = getUtility(IListManager).get(
- mailing_list).default_nonmember_action
+ self.moderation_action = getUtility(IListManager).get_by_list_id(
+ list_id).default_nonmember_action
def __repr__(self):
return '<Member: {0} on {1} as {2}>'.format(
- self.address, self.mailing_list, self.role)
+ self.address, self.mailing_list.fqdn_listname, self.role)
+
+ @property
+ def mailing_list(self):
+ """See `IMember`."""
+ list_manager = getUtility(IListManager)
+ return list_manager.get_by_list_id(self.list_id)
@property
def member_id(self):
@@ -176,7 +183,8 @@ class Member(Model):
# XXX Um, this is definitely wrong
return 'http://example.com/' + self.address.email
- def unsubscribe(self):
+ @dbconnection
+ def unsubscribe(self, store):
"""See `IMember`."""
- config.db.store.remove(self.preferences)
- config.db.store.remove(self)
+ store.remove(self.preferences)
+ store.remove(self)
diff --git a/src/mailman/model/message.py b/src/mailman/model/message.py
index 3345c64c9..190b4055c 100644
--- a/src/mailman/model/message.py
+++ b/src/mailman/model/message.py
@@ -17,8 +17,7 @@
"""Model for messages."""
-
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,28 +25,28 @@ __all__ = [
]
from storm.locals import AutoReload, Int, RawStr, Unicode
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessage
+@implementer(IMessage)
class Message(Model):
"""A message in the message store."""
- implements(IMessage)
-
id = Int(primary=True, default=AutoReload)
message_id = Unicode()
message_id_hash = RawStr()
path = RawStr()
# This is a Messge-ID field representation, not a database row id.
- def __init__(self, message_id, message_id_hash, path):
+ @dbconnection
+ def __init__(self, store, message_id, message_id_hash, path):
super(Message, self).__init__()
self.message_id = message_id
self.message_id_hash = message_id_hash
self.path = path
- config.db.store.add(self)
+ store.add(self)
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index 59490993b..156375e6f 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -17,8 +17,7 @@
"""Model for message stores."""
-
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,9 +30,10 @@ import base64
import hashlib
import cPickle as pickle
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
+from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessageStore
from mailman.model.message import Message
from mailman.utilities.filesystem import makedirs
@@ -46,10 +46,12 @@ EMPTYSTRING = ''
+@implementer(IMessageStore)
class MessageStore:
- implements(IMessageStore)
+ """See `IMessageStore`."""
- def add(self, message):
+ @dbconnection
+ def add(self, store, message):
# Ensure that the message has the requisite headers.
message_ids = message.get_all('message-id', [])
if len(message_ids) <> 1:
@@ -57,8 +59,7 @@ class MessageStore:
# Calculate and insert the X-Message-ID-Hash.
message_id = message_ids[0]
# Complain if the Message-ID already exists in the storage.
- existing = config.db.store.find(Message,
- Message.message_id == message_id).one()
+ existing = store.find(Message, Message.message_id == message_id).one()
if existing is not None:
raise ValueError(
'Message ID already exists in message store: {0}'.format(
@@ -104,34 +105,37 @@ class MessageStore:
with open(path) as fp:
return pickle.load(fp)
- def get_message_by_id(self, message_id):
- row = config.db.store.find(Message, message_id=message_id).one()
+ @dbconnection
+ def get_message_by_id(self, store, message_id):
+ row = store.find(Message, message_id=message_id).one()
if row is None:
return None
return self._get_message(row)
- def get_message_by_hash(self, message_id_hash):
+ @dbconnection
+ def get_message_by_hash(self, store, message_id_hash):
# It's possible the hash came from a message header, in which case it
# will be a Unicode. However when coming from source code, it may be
# an 8-string. Coerce to the latter if necessary; it must be
# US-ASCII.
if isinstance(message_id_hash, unicode):
message_id_hash = message_id_hash.encode('ascii')
- row = config.db.store.find(Message,
- message_id_hash=message_id_hash).one()
+ row = store.find(Message, message_id_hash=message_id_hash).one()
if row is None:
return None
return self._get_message(row)
@property
- def messages(self):
- for row in config.db.store.find(Message):
+ @dbconnection
+ def messages(self, store):
+ for row in store.find(Message):
yield self._get_message(row)
- def delete_message(self, message_id):
- row = config.db.store.find(Message, message_id=message_id).one()
+ @dbconnection
+ def delete_message(self, store, message_id):
+ row = store.find(Message, message_id=message_id).one()
if row is None:
raise LookupError(message_id)
path = os.path.join(config.MESSAGES_DIR, row.path)
os.remove(path)
- config.db.store.remove(row)
+ store.remove(row)
diff --git a/src/mailman/model/mime.py b/src/mailman/model/mime.py
index c611aab89..462bb9016 100644
--- a/src/mailman/model/mime.py
+++ b/src/mailman/model/mime.py
@@ -15,9 +15,9 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Module stuff."""
+"""The content filter."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,7 +26,7 @@ __all__ = [
from storm.locals import Int, Reference, Unicode
-from zope.interface import implements
+from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.types import Enum
@@ -34,9 +34,9 @@ from mailman.interfaces.mime import IContentFilter, FilterType
+@implementer(IContentFilter)
class ContentFilter(Model):
"""A single filter criteria."""
- implements(IContentFilter)
id = Int(primary=True)
diff --git a/src/mailman/model/pending.py b/src/mailman/model/pending.py
index 557361c6f..727e4f754 100644
--- a/src/mailman/model/pending.py
+++ b/src/mailman/model/pending.py
@@ -17,7 +17,7 @@
"""Implementations of the IPendable and IPending interfaces."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -32,11 +32,12 @@ import hashlib
from lazr.config import as_timedelta
from storm.locals import DateTime, Int, RawStr, ReferenceSet, Unicode
-from zope.interface import implements
+from zope.interface import implementer
from zope.interface.verify import verifyObject
from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.interfaces.pending import (
IPendable, IPended, IPendedKeyValue, IPendings)
from mailman.utilities.datetime import now
@@ -44,11 +45,10 @@ from mailman.utilities.modules import call_name
+@implementer(IPendedKeyValue)
class PendedKeyValue(Model):
"""A pended key/value pair, tied to a token."""
- implements(IPendedKeyValue)
-
def __init__(self, key, value):
self.key = key
self.value = value
@@ -59,11 +59,11 @@ class PendedKeyValue(Model):
pended_id = Int()
+
+@implementer(IPended)
class Pended(Model):
"""A pended event, tied to a token."""
- implements(IPended)
-
def __init__(self, token, expiration_date):
super(Pended, self).__init__()
self.token = token
@@ -76,17 +76,18 @@ class Pended(Model):
+@implementer(IPendable)
class UnpendedPendable(dict):
- implements(IPendable)
+ pass
+@implementer(IPendings)
class Pendings:
"""Implementation of the IPending interface."""
- implements(IPendings)
-
- def add(self, pendable, lifetime=None):
+ @dbconnection
+ def add(self, store, pendable, lifetime=None):
verifyObject(IPendable, pendable)
# Calculate the token and the lifetime.
if lifetime is None:
@@ -104,7 +105,7 @@ class Pendings:
token = hashlib.sha1(repr(x)).hexdigest()
# In practice, we'll never get a duplicate, but we'll be anal
# about checking anyway.
- if config.db.store.find(Pended, token=token).count() == 0:
+ if store.find(Pended, token=token).count() == 0:
break
else:
raise AssertionError('Could not find a valid pendings token')
@@ -129,11 +130,11 @@ class Pendings:
'\2'.join(value))
keyval = PendedKeyValue(key=key, value=value)
pending.key_values.add(keyval)
- config.db.store.add(pending)
+ store.add(pending)
return token
- def confirm(self, token, expunge=True):
- store = config.db.store
+ @dbconnection
+ def confirm(self, store, token, expunge=True):
# Token can come in as a unicode, but it's stored in the database as
# bytes. They must be ascii.
pendings = store.find(Pended, token=str(token))
@@ -158,8 +159,8 @@ class Pendings:
store.remove(pending)
return pendable
- def evict(self):
- store = config.db.store
+ @dbconnection
+ def evict(self, store):
right_now = now()
for pending in store.find(Pended):
if pending.expiration_date < right_now:
diff --git a/src/mailman/model/preferences.py b/src/mailman/model/preferences.py
index 234c7399e..fdc30a94d 100644
--- a/src/mailman/model/preferences.py
+++ b/src/mailman/model/preferences.py
@@ -17,7 +17,7 @@
"""Model for preferences."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
from storm.locals import Bool, Int, Unicode
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.types import Enum
@@ -37,8 +37,9 @@ from mailman.interfaces.preferences import IPreferences
+@implementer(IPreferences)
class Preferences(Model):
- implements(IPreferences)
+ """See `IPreferences`."""
id = Int(primary=True)
acknowledge_posts = Bool()
diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py
index 4a3efa67f..5eb940233 100644
--- a/src/mailman/model/requests.py
+++ b/src/mailman/model/requests.py
@@ -17,81 +17,95 @@
"""Implementations of the pending requests interfaces."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
]
+from cPickle import dumps, loads
from datetime import timedelta
from storm.locals import AutoReload, Int, RawStr, Reference, Unicode
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.requests import IListRequests, RequestType
+@implementer(IPendable)
class DataPendable(dict):
- implements(IPendable)
+ def update(self, mapping):
+ # Keys and values must be strings (unicodes, but bytes values are
+ # accepted for now). Any other types for keys are a programming
+ # error. If we find a non-Unicode value, pickle it and encode it in
+ # such a way that it will be properly reconstituted when unpended.
+ clean_mapping = {}
+ for key, value in mapping.items():
+ assert isinstance(key, basestring)
+ if not isinstance(value, unicode):
+ key = '_pck_' + key
+ value = dumps(value).decode('raw-unicode-escape')
+ clean_mapping[key] = value
+ super(DataPendable, self).update(clean_mapping)
+@implementer(IListRequests)
class ListRequests:
- implements(IListRequests)
def __init__(self, mailing_list):
self.mailing_list = mailing_list
@property
- def count(self):
- return config.db.store.find(
- _Request, mailing_list=self.mailing_list).count()
+ @dbconnection
+ def count(self, store):
+ return store.find(_Request, mailing_list=self.mailing_list).count()
- def count_of(self, request_type):
- return config.db.store.find(
+ @dbconnection
+ def count_of(self, store, request_type):
+ return store.find(
_Request,
mailing_list=self.mailing_list, request_type=request_type).count()
@property
- def held_requests(self):
- results = config.db.store.find(
- _Request, mailing_list=self.mailing_list)
+ @dbconnection
+ def held_requests(self, store):
+ results = store.find(_Request, mailing_list=self.mailing_list)
for request in results:
yield request
- def of_type(self, request_type):
- results = config.db.store.find(
+ @dbconnection
+ def of_type(self, store, request_type):
+ results = store.find(
_Request,
mailing_list=self.mailing_list, request_type=request_type)
for request in results:
yield request
- def hold_request(self, request_type, key, data=None):
+ @dbconnection
+ def hold_request(self, store, request_type, key, data=None):
if request_type not in RequestType:
raise TypeError(request_type)
if data is None:
data_hash = None
else:
- # We're abusing the pending database as a way of storing arbitrary
- # key/value pairs, where both are strings. This isn't ideal but
- # it lets us get auxiliary data almost for free. We may need to
- # lock this down more later.
pendable = DataPendable()
pendable.update(data)
token = getUtility(IPendings).add(pendable, timedelta(days=5000))
data_hash = token
request = _Request(key, request_type, self.mailing_list, data_hash)
- config.db.store.add(request)
+ store.add(request)
return request.id
- def get_request(self, request_id, request_type=None):
- result = config.db.store.get(_Request, request_id)
+ @dbconnection
+ def get_request(self, store, request_id, request_type=None):
+ result = store.get(_Request, request_id)
if result is None:
return None
if request_type is not None and result.request_type != request_type:
@@ -101,16 +115,22 @@ class ListRequests:
pendable = getUtility(IPendings).confirm(
result.data_hash, expunge=False)
data = dict()
- data.update(pendable)
+ # Unpickle any non-Unicode values.
+ for key, value in pendable.items():
+ if key.startswith('_pck_'):
+ data[key[5:]] = loads(value.encode('raw-unicode-escape'))
+ else:
+ data[key] = value
return result.key, data
- def delete_request(self, request_id):
- request = config.db.store.get(_Request, request_id)
+ @dbconnection
+ def delete_request(self, store, request_id):
+ request = store.get(_Request, request_id)
if request is None:
raise KeyError(request_id)
# Throw away the pended data.
getUtility(IPendings).confirm(request.data_hash)
- config.db.store.remove(request)
+ store.remove(request)
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 48d434ab1..f6f86fbeb 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -22,7 +22,7 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -38,9 +38,9 @@ __all__ = [
from storm.expr import And, Or
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
+from mailman.database.transaction import dbconnection
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.roster import IRoster
from mailman.model.address import Address
@@ -48,6 +48,7 @@ from mailman.model.member import Member
+@implementer(IRoster)
class AbstractRoster:
"""An abstract IRoster class.
@@ -57,18 +58,17 @@ class AbstractRoster:
This requires that subclasses implement the 'members' property.
"""
- implements(IRoster)
-
role = None
def __init__(self, mlist):
self._mlist = mlist
- def _query(self):
- return config.db.store.find(
+ @dbconnection
+ def _query(self, store):
+ return store.find(
Member,
- mailing_list=self._mlist.fqdn_listname,
- role=self.role)
+ Member.list_id == self._mlist.list_id,
+ Member.role == self.role)
@property
def members(self):
@@ -101,11 +101,12 @@ class AbstractRoster:
for member in self.members:
yield member.address
- def get_member(self, address):
+ @dbconnection
+ def get_member(self, store, address):
"""See `IRoster`."""
- results = config.db.store.find(
+ results = store.find(
Member,
- Member.mailing_list == self._mlist.fqdn_listname,
+ Member.list_id == self._mlist.list_id,
Member.role == self.role,
Address.email == address,
Member.address_id == Address.id)
@@ -157,18 +158,20 @@ class AdministratorRoster(AbstractRoster):
name = 'administrator'
- def _query(self):
- return config.db.store.find(
+ @dbconnection
+ def _query(self, store):
+ return store.find(
Member,
- Member.mailing_list == self._mlist.fqdn_listname,
+ Member.list_id == self._mlist.list_id,
Or(Member.role == MemberRole.owner,
Member.role == MemberRole.moderator))
- def get_member(self, address):
+ @dbconnection
+ def get_member(self, store, address):
"""See `IRoster`."""
- results = config.db.store.find(
+ results = store.find(
Member,
- Member.mailing_list == self._mlist.fqdn_listname,
+ Member.list_id == self._mlist.list_id,
Or(Member.role == MemberRole.moderator,
Member.role == MemberRole.owner),
Address.email == address,
@@ -194,7 +197,8 @@ class DeliveryMemberRoster(AbstractRoster):
# checking the delivery mode to a query parameter.
return len(tuple(self.members))
- def _get_members(self, *delivery_modes):
+ @dbconnection
+ def _get_members(self, store, *delivery_modes):
"""The set of members for a mailing list, filter by delivery mode.
:param delivery_modes: The modes to filter on.
@@ -202,9 +206,9 @@ class DeliveryMemberRoster(AbstractRoster):
:return: A generator of members.
:rtype: generator
"""
- results = config.db.store.find(
+ results = store.find(
Member,
- And(Member.mailing_list == self._mlist.fqdn_listname,
+ And(Member.list_id == self._mlist.list_id,
Member.role == MemberRole.member))
for member in results:
if member.delivery_mode in delivery_modes:
@@ -244,25 +248,24 @@ class Subscribers(AbstractRoster):
name = 'subscribers'
- def _query(self):
- return config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname)
+ @dbconnection
+ def _query(self, store):
+ return store.find(Member, Member.list_id == self._mlist.list_id)
+@implementer(IRoster)
class Memberships:
"""A roster of a single user's memberships."""
- implements(IRoster)
-
name = 'memberships'
def __init__(self, user):
self._user = user
- def _query(self):
- results = config.db.store.find(
+ @dbconnection
+ def _query(self, store):
+ results = store.find(
Member,
Or(Member.user_id == self._user.id,
And(Address.user_id == self._user.id,
@@ -291,9 +294,10 @@ class Memberships:
for address in self._user.addresses:
yield address
- def get_member(self, address):
+ @dbconnection
+ def get_member(self, store, address):
"""See `IRoster`."""
- results = config.db.store.find(
+ results = store.find(
Member,
Member.address_id == Address.id,
Address.user_id == self._user.id)
diff --git a/src/mailman/model/tests/test_bounce.py b/src/mailman/model/tests/test_bounce.py
index da2b661ea..377fab4cc 100644
--- a/src/mailman/model/tests/test_bounce.py
+++ b/src/mailman/model/tests/test_bounce.py
@@ -17,7 +17,7 @@
"""Test bounce model objects."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -30,7 +30,7 @@ from datetime import datetime
from zope.component import getUtility
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
from mailman.testing.helpers import (
specialized_message_from_string as message_from_string)
@@ -52,8 +52,9 @@ Message-Id: <first>
""")
def test_events_iterator(self):
- self._processor.register(self._mlist, 'anne@example.com', self._msg)
- config.db.commit()
+ with transaction():
+ self._processor.register(
+ self._mlist, 'anne@example.com', self._msg)
events = list(self._processor.events)
self.assertEqual(len(events), 1)
event = events[0]
@@ -75,23 +76,25 @@ Message-Id: <first>
self.assertEqual(event.processed, False)
def test_unprocessed_events_iterator(self):
- self._processor.register(self._mlist, 'anne@example.com', self._msg)
- self._processor.register(self._mlist, 'bart@example.com', self._msg)
- config.db.commit()
+ with transaction():
+ self._processor.register(
+ self._mlist, 'anne@example.com', self._msg)
+ self._processor.register(
+ self._mlist, 'bart@example.com', self._msg)
events = list(self._processor.events)
self.assertEqual(len(events), 2)
unprocessed = list(self._processor.unprocessed)
# The unprocessed list will be exactly the same right now.
self.assertEqual(len(unprocessed), 2)
# Process one of the events.
- events[0].processed = True
- config.db.commit()
+ with transaction():
+ events[0].processed = True
# Now there will be only one unprocessed event.
unprocessed = list(self._processor.unprocessed)
self.assertEqual(len(unprocessed), 1)
# Process the other event.
- events[1].processed = True
- config.db.commit()
+ with transaction():
+ events[1].processed = True
# Now there will be no unprocessed events.
unprocessed = list(self._processor.unprocessed)
self.assertEqual(len(unprocessed), 0)
diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py
index c3564aa40..08eae8aff 100644
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -17,7 +17,7 @@
"""Unique IDs."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,8 +28,8 @@ __all__ = [
from storm.locals import Int
from storm.properties import UUID
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
@@ -48,23 +48,29 @@ class UID(Model):
id = Int(primary=True)
uid = UUID()
- def __init__(self, uid):
+ @dbconnection
+ def __init__(self, store, uid):
super(UID, self).__init__()
self.uid = uid
- config.db.store.add(self)
+ store.add(self)
def __repr__(self):
return '<UID {0} at {1}>'.format(self.uid, id(self))
@staticmethod
- def record(uid):
+ @dbconnection
+ # Note that the parameter order is deliberate reversed here. Normally,
+ # `store` is the first parameter after `self`, but since this is a
+ # staticmethod and there is no self, the decorator will see the uid in
+ # arg[0].
+ def record(uid, store):
"""Record the uid in the database.
:param uid: The unique id.
:type uid: unicode
:raises ValueError: if the id is not unique.
"""
- existing = config.db.store.find(UID, uid=uid)
+ existing = store.find(UID, uid=uid)
if existing.count() != 0:
raise ValueError(uid)
return UID(uid)
diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py
index 9ca9b5aea..a723df44e 100644
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -17,7 +17,7 @@
"""Model for users."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,10 +28,10 @@ from storm.locals import (
DateTime, Int, RawStr, Reference, ReferenceSet, Unicode)
from storm.properties import UUID
from zope.event import notify
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.interfaces.address import (
AddressAlreadyLinkedError, AddressNotLinkedError)
from mailman.interfaces.user import (
@@ -47,11 +47,10 @@ uid_factory = UniqueIDFactory(context='users')
+@implementer(IUser)
class User(Model):
"""Mailman users."""
- implements(IUser)
-
id = Int(primary=True)
display_name = Unicode()
_password = RawStr(name='password')
@@ -64,16 +63,17 @@ class User(Model):
preferences_id = Int()
preferences = Reference(preferences_id, 'Preferences.id')
- def __init__(self, display_name=None, preferences=None):
+ @dbconnection
+ def __init__(self, store, display_name=None, preferences=None):
super(User, self).__init__()
self._created_on = date_factory.now()
user_id = uid_factory.new_uid()
- assert config.db.store.find(User, _user_id=user_id).count() == 0, (
+ assert store.find(User, _user_id=user_id).count() == 0, (
'Duplicate user id {0}'.format(user_id))
self._user_id = user_id
self.display_name = ('' if display_name is None else display_name)
self.preferences = preferences
- config.db.store.add(self)
+ store.add(self)
def __repr__(self):
short_user_id = self.user_id.int
@@ -135,18 +135,20 @@ class User(Model):
"""See `IUser`."""
self._preferred_address = None
- def controls(self, email):
+ @dbconnection
+ def controls(self, store, email):
"""See `IUser`."""
- found = config.db.store.find(Address, email=email)
+ found = store.find(Address, email=email)
if found.count() == 0:
return False
assert found.count() == 1, 'Unexpected count'
return found[0].user is self
- def register(self, email, display_name=None):
+ @dbconnection
+ def register(self, store, email, display_name=None):
"""See `IUser`."""
# First, see if the address already exists
- address = config.db.store.find(Address, email=email).one()
+ address = store.find(Address, email=email).one()
if address is None:
if display_name is None:
display_name = ''
diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py
index c8a5c65a2..4c7daaa59 100644
--- a/src/mailman/model/usermanager.py
+++ b/src/mailman/model/usermanager.py
@@ -17,7 +17,7 @@
"""A user manager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,9 +25,9 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
-from mailman.config import config
+from mailman.database.transaction import dbconnection
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.model.address import Address
@@ -37,8 +37,9 @@ from mailman.model.user import User
+@implementer(IUserManager)
class UserManager:
- implements(IUserManager)
+ """See `IUserManager`."""
def create_user(self, email=None, display_name=None):
"""See `IUserManager`."""
@@ -48,33 +49,38 @@ class UserManager:
user.link(address)
return user
- def delete_user(self, user):
+ @dbconnection
+ def delete_user(self, store, user):
"""See `IUserManager`."""
- config.db.store.remove(user)
+ store.remove(user)
- def get_user(self, email):
+ @dbconnection
+ def get_user(self, store, email):
"""See `IUserManager`."""
- addresses = config.db.store.find(Address, email=email.lower())
+ addresses = store.find(Address, email=email.lower())
if addresses.count() == 0:
return None
return addresses.one().user
- def get_user_by_id(self, user_id):
+ @dbconnection
+ def get_user_by_id(self, store, user_id):
"""See `IUserManager`."""
- users = config.db.store.find(User, _user_id=user_id)
+ users = store.find(User, _user_id=user_id)
if users.count() == 0:
return None
return users.one()
@property
- def users(self):
+ @dbconnection
+ def users(self, store):
"""See `IUserManager`."""
- for user in config.db.store.find(User):
+ for user in store.find(User):
yield user
- def create_address(self, email, display_name=None):
+ @dbconnection
+ def create_address(self, store, email, display_name=None):
"""See `IUserManager`."""
- addresses = config.db.store.find(Address, email=email.lower())
+ addresses = store.find(Address, email=email.lower())
if addresses.count() == 1:
found = addresses[0]
raise ExistingAddressError(found.original_email)
@@ -85,32 +91,36 @@ class UserManager:
# constructor will do the right thing.
address = Address(email, display_name)
address.preferences = Preferences()
- config.db.store.add(address)
+ store.add(address)
return address
- def delete_address(self, address):
+ @dbconnection
+ def delete_address(self, store, address):
"""See `IUserManager`."""
# If there's a user controlling this address, it has to first be
# unlinked before the address can be deleted.
if address.user:
address.user.unlink(address)
- config.db.store.remove(address)
+ store.remove(address)
- def get_address(self, email):
+ @dbconnection
+ def get_address(self, store, email):
"""See `IUserManager`."""
- addresses = config.db.store.find(Address, email=email.lower())
+ addresses = store.find(Address, email=email.lower())
if addresses.count() == 0:
return None
return addresses.one()
@property
- def addresses(self):
+ @dbconnection
+ def addresses(self, store):
"""See `IUserManager`."""
- for address in config.db.store.find(Address):
+ for address in store.find(Address):
yield address
@property
- def members(self):
+ @dbconnection
+ def members(self, store):
"""See `IUserManager."""
- for member in config.db.store.find(Member):
+ for member in store.find(Member):
yield member
diff --git a/src/mailman/model/version.py b/src/mailman/model/version.py
index d6a4f3938..8b4dcae89 100644
--- a/src/mailman/model/version.py
+++ b/src/mailman/model/version.py
@@ -17,7 +17,7 @@
"""Model class for version numbers."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -34,6 +34,10 @@ class Version(Model):
component = Unicode()
version = Unicode()
+ # The testing machinery will generally reset all tables, however because
+ # this table tracks schema migrations, we do not want to reset it.
+ PRESERVE = True
+
def __init__(self, component, version):
super(Version, self).__init__()
self.component = component
diff --git a/src/mailman/mta/aliases.py b/src/mailman/mta/aliases.py
index 573160d3c..e87f5880f 100644
--- a/src/mailman/mta/aliases.py
+++ b/src/mailman/mta/aliases.py
@@ -17,7 +17,7 @@
"""Utility for generating all the aliases of a mailing list."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,7 +25,7 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.interfaces.mta import IMailTransportAgentAliases
@@ -43,11 +43,10 @@ SUBDESTINATIONS = (
+@implementer(IMailTransportAgentAliases)
class MailTransportAgentAliases:
"""Utility for generating all the aliases of a mailing list."""
- implements(IMailTransportAgentAliases)
-
def aliases(self, mlist):
"""See `IMailTransportAgentAliases`."""
# Always return
diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py
index 8068eaec1..e11c37ff6 100644
--- a/src/mailman/mta/base.py
+++ b/src/mailman/mta/base.py
@@ -17,7 +17,7 @@
"""Base delivery class."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,7 +31,7 @@ import socket
import logging
import smtplib
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.mta import IMailTransportAgentDelivery
@@ -42,11 +42,10 @@ log = logging.getLogger('mailman.smtp')
+@implementer(IMailTransportAgentDelivery)
class BaseDelivery:
"""Base delivery class."""
- implements(IMailTransportAgentDelivery)
-
def __init__(self):
"""Create a basic deliverer."""
username = (config.mta.smtp_user if config.mta.smtp_user else None)
diff --git a/src/mailman/mta/null.py b/src/mailman/mta/null.py
index 6b2f76a1c..c94fa1015 100644
--- a/src/mailman/mta/null.py
+++ b/src/mailman/mta/null.py
@@ -20,7 +20,7 @@
Exim one example of an MTA that Just Works.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,16 +28,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
+
from mailman.interfaces.mta import IMailTransportAgentLifecycle
+@implementer(IMailTransportAgentLifecycle)
class NullMTA:
"""Null MTA that just satisfies the interface."""
- implements(IMailTransportAgentLifecycle)
-
def create(self, mlist):
"""See `IMailTransportAgentLifecycle`."""
pass
diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py
index 32bdb8268..c04e38f02 100644
--- a/src/mailman/mta/postfix.py
+++ b/src/mailman/mta/postfix.py
@@ -17,7 +17,7 @@
"""Creation/deletion hooks for the Postfix MTA."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,7 +31,7 @@ import logging
from flufl.lock import Lock
from operator import attrgetter
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.listmanager import IListManager
@@ -55,11 +55,10 @@ class _FakeList:
+@implementer(IMailTransportAgentLifecycle)
class LMTP:
"""Connect Mailman to Postfix via LMTP."""
- implements(IMailTransportAgentLifecycle)
-
def create(self, mlist):
"""See `IMailTransportAgentLifecycle`."""
# We can ignore the mlist argument because for LMTP delivery, we just
@@ -117,23 +116,24 @@ class LMTP:
for list_name, mail_host in list_manager.name_components:
mlist = _FakeList(list_name, mail_host)
by_domain.setdefault(mlist.mail_host, []).append(mlist)
- print >> fp, """\
+ print("""\
# AUTOMATICALLY GENERATED BY MAILMAN ON {0}
#
# This file is generated by Mailman, and is kept in sync with the binary hash
# file. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you know what you're
# doing, and can keep the two files properly in sync. If you screw it up,
# you're on your own.
-""".format(now().replace(microsecond=0))
+""".format(now().replace(microsecond=0)), file=fp)
sort_key = attrgetter('list_name')
for domain in sorted(by_domain):
- print >> fp, """\
-# Aliases which are visible only in the @{0} domain.""".format(domain)
+ print("""\
+# Aliases which are visible only in the @{0} domain.""".format(domain),
+ file=fp)
for mlist in sorted(by_domain[domain], key=sort_key):
utility = getUtility(IMailTransportAgentAliases)
aliases = list(utility.aliases(mlist))
width = max(len(alias) for alias in aliases) + 3
- print >> fp, ALIASTMPL.format(aliases.pop(0), config, width)
+ print(ALIASTMPL.format(aliases.pop(0), config, width), file=fp)
for alias in aliases:
- print >> fp, ALIASTMPL.format(alias, config, width)
- print >> fp
+ print(ALIASTMPL.format(alias, config, width), file=fp)
+ print(file=fp)
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py
index e791dcfbd..2e81cb030 100644
--- a/src/mailman/rest/addresses.py
+++ b/src/mailman/rest/addresses.py
@@ -139,7 +139,7 @@ class UserAddresses(_AddressBase):
def membership_key(member):
# Sort first by mailing list, then by address, then by role.
- return member.mailing_list, member.address.email, int(member.role)
+ return member.list_id, member.address.email, int(member.role)
class AddressMemberships(MemberCollection):
diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py
index d6b27cc6c..83d4c74f6 100644
--- a/src/mailman/rest/configuration.py
+++ b/src/mailman/rest/configuration.py
@@ -17,7 +17,7 @@
"""Mailing list configuration via REST API."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -30,6 +30,7 @@ from restish import http, resource
from mailman.config import config
from mailman.interfaces.action import Action
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging
from mailman.rest.helpers import PATCH, etag, no_content
@@ -170,6 +171,7 @@ ATTRIBUTES = dict(
autoresponse_owner_text=GetterSetter(unicode),
autoresponse_postings_text=GetterSetter(unicode),
autoresponse_request_text=GetterSetter(unicode),
+ archive_policy=GetterSetter(enum_validator(ArchivePolicy)),
bounces_address=GetterSetter(None),
collapse_alternatives=GetterSetter(as_boolean),
convert_html_to_plaintext=GetterSetter(as_boolean),
@@ -181,9 +183,8 @@ ATTRIBUTES = dict(
digest_size_threshold=GetterSetter(float),
filter_content=GetterSetter(as_boolean),
fqdn_listname=GetterSetter(None),
- generic_nonmember_action=GetterSetter(int),
mail_host=GetterSetter(None),
- include_list_post_header=GetterSetter(as_boolean),
+ allow_list_posts=GetterSetter(as_boolean),
include_rfc2369_headers=GetterSetter(as_boolean),
join_address=GetterSetter(None),
last_post_at=GetterSetter(None),
diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst
index a8f875d12..cb9242d2b 100644
--- a/src/mailman/rest/docs/addresses.rst
+++ b/src/mailman/rest/docs/addresses.rst
@@ -170,16 +170,16 @@ Elle can get her memberships for each of her email addresses.
entry 0:
address: elle@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/2
entry 1:
address: elle@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: "..."
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -207,16 +207,16 @@ does not show up in the list of memberships for his other address.
entry 0:
address: elle@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/2
entry 1:
address: elle@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: "..."
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -229,8 +229,8 @@ does not show up in the list of memberships for his other address.
entry 0:
address: eperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: "..."
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/2
diff --git a/src/mailman/rest/docs/configuration.rst b/src/mailman/rest/docs/configuration.rst
index 676b3426c..4507345c3 100644
--- a/src/mailman/rest/docs/configuration.rst
+++ b/src/mailman/rest/docs/configuration.rst
@@ -20,7 +20,9 @@ All readable attributes for a list are available on a sub-resource.
admin_notify_mchanges: False
administrivia: True
advertised: True
+ allow_list_posts: True
anonymous_list: False
+ archive_policy: public
autorespond_owner: none
autorespond_postings: none
autorespond_requests: none
@@ -40,9 +42,7 @@ All readable attributes for a list are available on a sub-resource.
display_name: Test-one
filter_content: False
fqdn_listname: test-one@example.com
- generic_nonmember_action: 1
http_etag: "..."
- include_list_post_header: True
include_rfc2369_headers: True
join_address: test-one-join@example.com
last_post_at: None
@@ -81,6 +81,7 @@ all the writable attributes in one request.
... administrivia=False,
... advertised=False,
... anonymous_list=True,
+ ... archive_policy='never',
... autorespond_owner='respond_and_discard',
... autorespond_postings='respond_and_continue',
... autorespond_requests='respond_and_discard',
@@ -91,7 +92,7 @@ all the writable attributes in one request.
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='virgin',
... filter_content=True,
@@ -102,7 +103,6 @@ all the writable attributes in one request.
... welcome_message_uri='mailman:///welcome.txt',
... default_member_action='hold',
... default_nonmember_action='discard',
- ... generic_nonmember_action=2,
... ),
... 'PUT')
content-length: 0
@@ -119,7 +119,9 @@ These values are changed permanently.
admin_notify_mchanges: True
administrivia: False
advertised: False
+ allow_list_posts: False
anonymous_list: True
+ archive_policy: never
autorespond_owner: respond_and_discard
autorespond_postings: respond_and_continue
autorespond_requests: respond_and_discard
@@ -139,7 +141,6 @@ These values are changed permanently.
display_name: Fnords
filter_content: True
...
- include_list_post_header: False
include_rfc2369_headers: False
...
posting_pipeline: virgin
@@ -161,6 +162,7 @@ must be included. It is an error to leave one or more out...
... administrivia=False,
... advertised=False,
... anonymous_list=True,
+ ... archive_policy='public',
... autorespond_owner='respond_and_discard',
... autorespond_postings='respond_and_continue',
... autorespond_requests='respond_and_discard',
@@ -171,7 +173,7 @@ must be included. It is an error to leave one or more out...
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='virgin',
... filter_content=True,
@@ -182,7 +184,6 @@ must be included. It is an error to leave one or more out...
... welcome_message_uri='welcome message',
... default_member_action='accept',
... default_nonmember_action='accept',
- ... generic_nonmember_action=2,
... ),
... 'PUT')
Traceback (most recent call last):
@@ -201,6 +202,7 @@ must be included. It is an error to leave one or more out...
... administrivia=False,
... advertised=False,
... anonymous_list=True,
+ ... archive_policy='public',
... autorespond_owner='respond_and_discard',
... autorespond_postings='respond_and_continue',
... autorespond_requests='respond_and_discard',
@@ -211,7 +213,7 @@ must be included. It is an error to leave one or more out...
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='virgin',
... filter_content=True,
@@ -244,7 +246,7 @@ It is also an error to spell an attribute value incorrectly...
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='virgin',
... filter_content=True,
@@ -266,6 +268,7 @@ It is also an error to spell an attribute value incorrectly...
... admin_notify_mchanges=True,
... advertised=False,
... anonymous_list=True,
+ ... archive_policy='public',
... autorespond_owner='respond_and_discard',
... autorespond_postings='respond_and_continue',
... autorespond_requests='respond_and_discard',
@@ -276,7 +279,7 @@ It is also an error to spell an attribute value incorrectly...
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='dummy',
... filter_content=True,
@@ -308,7 +311,7 @@ It is also an error to spell an attribute value incorrectly...
... display_name='Fnords',
... description='This is my mailing list',
... include_rfc2369_headers=False,
- ... include_list_post_header=False,
+ ... allow_list_posts=False,
... digest_size_threshold=10.5,
... posting_pipeline='virgin',
... filter_content=True,
diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst
index 860d33c21..6189990cb 100644
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -43,8 +43,8 @@ the REST interface.
entry 0:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
@@ -57,8 +57,8 @@ Bart's specific membership can be accessed directly:
>>> dump_json('http://localhost:9001/3.0/members/1')
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
@@ -71,16 +71,16 @@ the REST interface.
entry 0:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
entry 1:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -98,24 +98,24 @@ subscribes, she is returned first.
entry 0:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/3
entry 1:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
entry 2:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -137,40 +137,40 @@ User ids are different than member ids.
entry 0:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/4
user: http://localhost:9001/3.0/users/3
entry 1:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/5
user: http://localhost:9001/3.0/users/2
entry 2:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/3
entry 3:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
entry 4:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -185,16 +185,16 @@ We can also get just the members of a single mailing list.
entry 0:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/4
user: http://localhost:9001/3.0/users/3
entry 1:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/5
user: http://localhost:9001/3.0/users/2
@@ -212,7 +212,7 @@ mailing list.
::
>>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'ant@example.com',
+ ... 'list_id': 'ant.example.com',
... 'subscriber': 'dperson@example.com',
... 'role': 'moderator',
... })
@@ -223,7 +223,7 @@ mailing list.
status: 201
>>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'bee@example.com',
+ ... 'list_id': 'bee.example.com',
... 'subscriber': 'cperson@example.com',
... 'role': 'owner',
... })
@@ -237,56 +237,56 @@ mailing list.
entry 0:
address: dperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: moderator
self_link: http://localhost:9001/3.0/members/6
user: http://localhost:9001/3.0/users/4
entry 1:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/4
user: http://localhost:9001/3.0/users/3
entry 2:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/5
user: http://localhost:9001/3.0/users/2
entry 3:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: owner
self_link: http://localhost:9001/3.0/members/7
user: http://localhost:9001/3.0/users/2
entry 4:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/3
entry 5:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
entry 6:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -301,8 +301,8 @@ We can access all the owners of a list.
entry 0:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: owner
self_link: http://localhost:9001/3.0/members/7
user: http://localhost:9001/3.0/users/2
@@ -320,8 +320,8 @@ A specific member can always be referenced by their role and address.
... 'bee@example.com/owner/cperson@example.com')
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: owner
self_link: http://localhost:9001/3.0/members/7
user: http://localhost:9001/3.0/users/2
@@ -335,16 +335,16 @@ example, we can search for all the memberships of a particular address.
entry 0:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/4
user: http://localhost:9001/3.0/users/3
entry 1:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/3
@@ -355,37 +355,37 @@ example, we can search for all the memberships of a particular address.
Or, we can find all the memberships for a particular mailing list.
>>> dump_json('http://localhost:9001/3.0/members/find', {
- ... 'fqdn_listname': 'bee@example.com',
+ ... 'list_id': 'bee.example.com',
... })
entry 0:
address: aperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/3
user: http://localhost:9001/3.0/users/3
entry 1:
address: bperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/1
user: http://localhost:9001/3.0/users/1
entry 2:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
entry 3:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: owner
self_link: http://localhost:9001/3.0/members/7
user: http://localhost:9001/3.0/users/2
@@ -398,21 +398,21 @@ list.
>>> dump_json('http://localhost:9001/3.0/members/find', {
... 'subscriber': 'cperson@example.com',
- ... 'fqdn_listname': 'bee@example.com',
+ ... 'list_id': 'bee.example.com',
... })
entry 0:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
entry 1:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: owner
self_link: http://localhost:9001/3.0/members/7
user: http://localhost:9001/3.0/users/2
@@ -429,16 +429,16 @@ Or, we can find all the memberships for an address with a specific role.
entry 0:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/5
user: http://localhost:9001/3.0/users/2
entry 1:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -450,14 +450,14 @@ Finally, we can search for a specific member given all three criteria.
>>> dump_json('http://localhost:9001/3.0/members/find', {
... 'subscriber': 'cperson@example.com',
- ... 'fqdn_listname': 'bee@example.com',
+ ... 'list_id': 'bee.example.com',
... 'role': 'member',
... })
entry 0:
address: cperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: ...
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/2
user: http://localhost:9001/3.0/users/2
@@ -478,7 +478,7 @@ address is not yet known to Mailman, a user is created for her. By default,
get gets a regular delivery.
>>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'ant@example.com',
+ ... 'list_id': 'ant.example.com',
... 'subscriber': 'eperson@example.com',
... 'display_name': 'Elly Person',
... })
@@ -495,8 +495,8 @@ Elly is now a known user, and a member of the mailing list.
>>> elly
<User "Elly Person" (...) at ...>
- >>> set(member.mailing_list for member in elly.memberships.members)
- set([u'ant@example.com'])
+ >>> set(member.list_id for member in elly.memberships.members)
+ set([u'ant.example.com'])
>>> dump_json('http://localhost:9001/3.0/members')
entry 0:
@@ -504,8 +504,8 @@ Elly is now a known user, and a member of the mailing list.
entry 3:
address: eperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: ...
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/8
user: http://localhost:9001/3.0/users/5
@@ -530,7 +530,7 @@ list with her preferred address.
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'ant@example.com',
+ ... 'list_id': 'ant.example.com',
... 'subscriber': user_id,
... })
content-length: 0
@@ -545,8 +545,8 @@ list with her preferred address.
entry 4:
address: gwen@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/9
user: http://localhost:9001/3.0/users/6
@@ -568,8 +568,8 @@ the new address.
entry 4:
address: gwen.person@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/9
user: http://localhost:9001/3.0/users/6
@@ -606,7 +606,7 @@ Fred joins the `ant` mailing list but wants MIME digest delivery.
>>> transaction.abort()
>>> dump_json('http://localhost:9001/3.0/members', {
- ... 'fqdn_listname': 'ant@example.com',
+ ... 'list_id': 'ant.example.com',
... 'subscriber': 'fperson@example.com',
... 'display_name': 'Fred Person',
... 'delivery_mode': 'mime_digests',
@@ -633,8 +633,8 @@ Fred is getting MIME deliveries.
>>> dump_json('http://localhost:9001/3.0/members/10')
address: fperson@example.com
delivery_mode: mime_digests
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/10
user: http://localhost:9001/3.0/users/7
@@ -655,8 +655,8 @@ This can be done by PATCH'ing his member with the `delivery_mode` parameter.
>>> dump_json('http://localhost:9001/3.0/members/10')
address: fperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/10
user: http://localhost:9001/3.0/users/7
@@ -673,8 +673,8 @@ If a PATCH request changes no attributes, nothing happens.
>>> dump_json('http://localhost:9001/3.0/members/10')
address: fperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/10
user: http://localhost:9001/3.0/users/7
@@ -715,8 +715,8 @@ addresses.
entry 5:
address: herb@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/11
user: http://localhost:9001/3.0/users/8
@@ -724,8 +724,8 @@ addresses.
entry 10:
address: herb@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: "..."
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/12
user: http://localhost:9001/3.0/users/8
@@ -780,16 +780,16 @@ his membership ids have not changed.
entry 0:
address: hperson@example.com
delivery_mode: regular
- fqdn_listname: ant@example.com
http_etag: "..."
+ list_id: ant.example.com
role: member
self_link: http://localhost:9001/3.0/members/11
user: http://localhost:9001/3.0/users/8
entry 1:
address: hperson@example.com
delivery_mode: regular
- fqdn_listname: bee@example.com
http_etag: "..."
+ list_id: bee.example.com
role: member
self_link: http://localhost:9001/3.0/members/12
user: http://localhost:9001/3.0/users/8
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index a20306e17..cdede10ee 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -94,7 +94,7 @@ It is also available via the location given in the response.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -105,7 +105,7 @@ them with user ids. Thus, a user can be retrieved via its email address.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -129,7 +129,7 @@ therefore cannot be retrieved. It can be reset though.
created_on: 2005-08-01T07:49:23
display_name: Cris Person
http_etag: "..."
- password: {CLEARTEXT}...
+ password: {plaintext}...
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -227,7 +227,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -235,7 +235,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -243,7 +243,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -251,6 +251,6 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 3374e8f73..f25133211 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -137,7 +137,7 @@ class AList(_ListBase):
"""Delete the named mailing list."""
if self._mlist is None:
return http.not_found()
- remove_list(self._mlist.fqdn_listname, self._mlist)
+ remove_list(self._mlist)
return no_content()
@resource.child(member_matcher)
@@ -146,7 +146,7 @@ class AList(_ListBase):
if self._mlist is None:
return http.not_found()
members = getUtility(ISubscriptionService).find_members(
- email, self._mlist.fqdn_listname, role)
+ email, self._mlist.list_id, role)
if len(members) == 0:
return http.not_found()
assert len(members) == 1, 'Too many matches'
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 761e3147c..c6aaa7e39 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -61,7 +61,7 @@ class _MemberBase(resource.Resource, CollectionMixin):
user_id = member.user.user_id.int
member_id = member.member_id.int
return dict(
- fqdn_listname=member.mailing_list,
+ list_id=member.list_id,
address=member.address.email,
role=role,
user=path_to('users/{0}'.format(user_id)),
@@ -148,7 +148,7 @@ class AMember(_MemberBase):
# an admin or user notification.
if self._member is None:
return http.not_found()
- mlist = getUtility(IListManager).get(self._member.mailing_list)
+ mlist = getUtility(IListManager).get_by_list_id(self._member.list_id)
if self._member.role is MemberRole.member:
try:
delete_member(mlist, self._member.address.email, False, False)
@@ -197,7 +197,7 @@ class AllMembers(_MemberBase):
service = getUtility(ISubscriptionService)
try:
validator = Validator(
- fqdn_listname=unicode,
+ list_id=unicode,
subscriber=subscriber_validator,
display_name=unicode,
delivery_mode=enum_validator(DeliveryMode),
@@ -248,10 +248,10 @@ class FindMembers(_MemberBase):
"""Find a member"""
service = getUtility(ISubscriptionService)
validator = Validator(
- fqdn_listname=unicode,
+ list_id=unicode,
subscriber=unicode,
role=enum_validator(MemberRole),
- _optional=('fqdn_listname', 'subscriber', 'role'))
+ _optional=('list_id', 'subscriber', 'role'))
members = service.find_members(**validator(request))
# We can't just return the _FoundMembers instance, because
# CollectionMixins have only a GET method, which is incompatible with
diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py
index f4e7cab62..385b83912 100644
--- a/src/mailman/rest/tests/test_addresses.py
+++ b/src/mailman/rest/tests/test_addresses.py
@@ -17,10 +17,11 @@
"""REST address tests."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestAddresses',
]
@@ -29,7 +30,7 @@ import unittest
from urllib2 import HTTPError
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -39,8 +40,8 @@ class TestAddresses(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
def test_membership_of_missing_address(self):
# Try to get the memberships of a missing address.
diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py
index 89cc34630..a86768481 100644
--- a/src/mailman/rest/tests/test_domains.py
+++ b/src/mailman/rest/tests/test_domains.py
@@ -17,10 +17,11 @@
"""REST domain tests."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestDomains',
]
@@ -30,7 +31,7 @@ from urllib2 import HTTPError
from zope.component import getUtility
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.listmanager import IListManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -41,8 +42,8 @@ class TestDomains(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
def test_bogus_endpoint_extension(self):
# /domains/<domain>/lists/<anything> is not a valid endpoint.
@@ -67,8 +68,8 @@ class TestDomains(unittest.TestCase):
def test_lists_are_deleted_when_domain_is_deleted(self):
# /domains/<domain> DELETE removes all associated mailing lists.
- create_list('ant@example.com')
- config.db.commit()
+ with transaction():
+ create_list('ant@example.com')
content, response = call_api(
'http://localhost:9001/3.0/domains/example.com', method='DELETE')
self.assertEqual(response.status, 204)
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index b030a2e8d..cd0ebaf8e 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -32,7 +32,7 @@ from urllib2 import HTTPError
from zope.component import getUtility
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -96,8 +96,8 @@ class TestLists(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
self._usermanager = getUtility(IUserManager)
def test_member_count_with_no_members(self):
@@ -109,9 +109,9 @@ class TestLists(unittest.TestCase):
def test_member_count_with_one_member(self):
# Add a member to a list and check that the resource reflects this.
- anne = self._usermanager.create_address('anne@example.com')
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
resource, response = call_api(
'http://localhost:9001/3.0/lists/test@example.com')
self.assertEqual(response.status, 200)
@@ -119,11 +119,11 @@ class TestLists(unittest.TestCase):
def test_member_count_with_two_members(self):
# Add two members to a list and check that the resource reflects this.
- anne = self._usermanager.create_address('anne@example.com')
- self._mlist.subscribe(anne)
- bart = self._usermanager.create_address('bar@example.com')
- self._mlist.subscribe(bart)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
+ bart = self._usermanager.create_address('bar@example.com')
+ self._mlist.subscribe(bart)
resource, response = call_api(
'http://localhost:9001/3.0/lists/test@example.com')
self.assertEqual(response.status, 200)
diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py
index 202e5f057..18469e537 100644
--- a/src/mailman/rest/tests/test_membership.py
+++ b/src/mailman/rest/tests/test_membership.py
@@ -17,10 +17,11 @@
"""REST membership tests."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestMembership',
]
@@ -31,6 +32,7 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -42,8 +44,8 @@ class TestMembership(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
self._usermanager = getUtility(IUserManager)
def test_try_to_join_missing_list(self):
@@ -51,7 +53,7 @@ class TestMembership(unittest.TestCase):
try:
# For Python 2.6.
call_api('http://localhost:9001/3.0/members', {
- 'fqdn_listname': 'missing@example.com',
+ 'list_id': 'missing.example.com',
'subscriber': 'nobody@example.com',
})
except HTTPError as exc:
@@ -85,9 +87,9 @@ class TestMembership(unittest.TestCase):
raise AssertionError('Expected HTTPError')
def test_try_to_leave_a_list_twice(self):
- anne = self._usermanager.create_address('anne@example.com')
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
url = 'http://localhost:9001/3.0/members/1'
content, response = call_api(url, method='DELETE')
# For a successful DELETE, the response code is 204 and there is no
@@ -104,13 +106,13 @@ class TestMembership(unittest.TestCase):
raise AssertionError('Expected HTTPError')
def test_try_to_join_a_list_twice(self):
- anne = self._usermanager.create_address('anne@example.com')
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
try:
# For Python 2.6.
call_api('http://localhost:9001/3.0/members', {
- 'fqdn_listname': 'test@example.com',
+ 'list_id': 'test.example.com',
'subscriber': 'anne@example.com',
})
except HTTPError as exc:
@@ -122,7 +124,7 @@ class TestMembership(unittest.TestCase):
def test_join_with_invalid_delivery_mode(self):
try:
call_api('http://localhost:9001/3.0/members', {
- 'fqdn_listname': 'test@example.com',
+ 'list_id': 'test.example.com',
'subscriber': 'anne@example.com',
'display_name': 'Anne Person',
'delivery_mode': 'invalid-mode',
@@ -136,7 +138,7 @@ class TestMembership(unittest.TestCase):
def test_join_email_contains_slash(self):
content, response = call_api('http://localhost:9001/3.0/members', {
- 'fqdn_listname': 'test@example.com',
+ 'list_id': 'test.example.com',
'subscriber': 'hugh/person@example.com',
'display_name': 'Hugh Person',
})
@@ -151,12 +153,12 @@ class TestMembership(unittest.TestCase):
self.assertEqual(members[0].address.email, 'hugh/person@example.com')
def test_join_as_user_with_preferred_address(self):
- anne = self._usermanager.create_user('anne@example.com')
- preferred = list(anne.addresses)[0]
- preferred.verified_on = now()
- anne.preferred_address = preferred
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_user('anne@example.com')
+ preferred = list(anne.addresses)[0]
+ preferred.verified_on = now()
+ anne.preferred_address = preferred
+ self._mlist.subscribe(anne)
content, response = call_api('http://localhost:9001/3.0/members')
self.assertEqual(response.status, 200)
self.assertEqual(int(content['total_size']), 1)
@@ -166,15 +168,15 @@ class TestMembership(unittest.TestCase):
self.assertEqual(entry_0['role'], 'member')
self.assertEqual(entry_0['user'], 'http://localhost:9001/3.0/users/1')
self.assertEqual(entry_0['address'], 'anne@example.com')
- self.assertEqual(entry_0['fqdn_listname'], 'test@example.com')
+ self.assertEqual(entry_0['list_id'], 'test.example.com')
def test_member_changes_preferred_address(self):
- anne = self._usermanager.create_user('anne@example.com')
- preferred = list(anne.addresses)[0]
- preferred.verified_on = now()
- anne.preferred_address = preferred
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_user('anne@example.com')
+ preferred = list(anne.addresses)[0]
+ preferred.verified_on = now()
+ anne.preferred_address = preferred
+ self._mlist.subscribe(anne)
# Take a look at Anne's current membership.
content, response = call_api('http://localhost:9001/3.0/members')
self.assertEqual(int(content['total_size']), 1)
@@ -182,10 +184,10 @@ class TestMembership(unittest.TestCase):
self.assertEqual(entry_0['address'], 'anne@example.com')
# Anne registers a new address and makes it her preferred address.
# There are no changes to her membership.
- new_preferred = anne.register('aperson@example.com')
- new_preferred.verified_on = now()
- anne.preferred_address = new_preferred
- config.db.commit()
+ with transaction():
+ new_preferred = anne.register('aperson@example.com')
+ new_preferred.verified_on = now()
+ anne.preferred_address = new_preferred
# Take another look at Anne's current membership.
content, response = call_api('http://localhost:9001/3.0/members')
self.assertEqual(int(content['total_size']), 1)
@@ -214,9 +216,9 @@ class TestMembership(unittest.TestCase):
def test_patch_member_bogus_attribute(self):
# /members/<id> PATCH 'bogus' returns 400
- anne = self._usermanager.create_address('anne@example.com')
- self._mlist.subscribe(anne)
- config.db.commit()
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
try:
# For Python 2.6
call_api('http://localhost:9001/3.0/members/1', {
diff --git a/src/mailman/rest/tests/test_moderation.py b/src/mailman/rest/tests/test_moderation.py
index 79b0c8b80..dfcedef05 100644
--- a/src/mailman/rest/tests/test_moderation.py
+++ b/src/mailman/rest/tests/test_moderation.py
@@ -31,6 +31,7 @@ from urllib2 import HTTPError
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message, hold_subscription
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.member import DeliveryMode
from mailman.testing.helpers import (
call_api, specialized_message_from_string as mfs)
@@ -42,7 +43,8 @@ class TestModeration(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('ant@example.com')
+ with transaction():
+ self._mlist = create_list('ant@example.com')
self._msg = mfs("""\
From: anne@example.com
To: ant@example.com
@@ -51,7 +53,6 @@ Message-ID: <alpha>
Something else.
""")
- config.db.commit()
def test_not_found(self):
# When a bogus mailing list is given, 404 should result.
diff --git a/src/mailman/rest/tests/test_users.py b/src/mailman/rest/tests/test_users.py
index 1630eb96a..301027885 100644
--- a/src/mailman/rest/tests/test_users.py
+++ b/src/mailman/rest/tests/test_users.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestUsers',
]
@@ -29,7 +30,7 @@ import unittest
from urllib2 import HTTPError
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -39,8 +40,8 @@ class TestUsers(unittest.TestCase):
layer = RESTLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
def test_delete_bogus_user(self):
# Try to delete a user that does not exist.
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 4e1362120..bce541ae5 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -26,7 +26,7 @@ __all__ = [
]
-from flufl.password import lookup, make_secret, generate
+from passlib.utils import generate_password as generate
from restish import http, resource
from uuid import UUID
from zope.component import getUtility
@@ -102,8 +102,7 @@ class AllUsers(_UserBase):
if password is None:
# This will have to be reset since it cannot be retrieved.
password = generate(int(config.passwords.password_length))
- scheme = lookup(config.passwords.password_scheme.upper())
- user.password = make_secret(password, scheme)
+ user.password = config.password_context.encrypt(password)
location = path_to('users/{0}'.format(user.user_id.int))
return http.created(location, [], None)
diff --git a/src/mailman/rest/wsgiapp.py b/src/mailman/rest/wsgiapp.py
index 36e8ae5ac..a735d2012 100644
--- a/src/mailman/rest/wsgiapp.py
+++ b/src/mailman/rest/wsgiapp.py
@@ -33,6 +33,7 @@ from wsgiref.simple_server import WSGIRequestHandler
from wsgiref.simple_server import make_server as wsgi_server
from mailman.config import config
+from mailman.database.transaction import transactional
from mailman.rest.root import Root
@@ -51,17 +52,11 @@ class AdminWebServiceWSGIRequestHandler(WSGIRequestHandler):
class AdminWebServiceApplication(RestishApp):
"""Connect the restish WSGI application to Mailman's database."""
+ @transactional
def __call__(self, environ, start_response):
"""See `RestishApp`."""
- try:
- response = super(AdminWebServiceApplication, self).__call__(
- environ, start_response)
- except:
- config.db.abort()
- raise
- else:
- config.db.commit()
- return response
+ return super(AdminWebServiceApplication, self).__call__(
+ environ, start_response)
diff --git a/src/mailman/rules/administrivia.py b/src/mailman/rules/administrivia.py
index 41c6edf30..4c49e4ff2 100644
--- a/src/mailman/rules/administrivia.py
+++ b/src/mailman/rules/administrivia.py
@@ -17,7 +17,7 @@
"""The administrivia rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,7 +26,7 @@ __all__ = [
from email.iterators import typed_subpart_iterator
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -53,9 +53,9 @@ EMAIL_COMMANDS = {
+@implementer(IRule)
class Administrivia:
"""The administrivia rule."""
- implements(IRule)
name = 'administrivia'
description = _('Catch mis-addressed email commands.')
diff --git a/src/mailman/rules/any.py b/src/mailman/rules/any.py
index b0d147bec..33c20394d 100644
--- a/src/mailman/rules/any.py
+++ b/src/mailman/rules/any.py
@@ -17,7 +17,7 @@
"""Check if any previous rules have matched."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class Any:
"""Look for any previous rule match."""
- implements(IRule)
name = 'any'
description = _('Look for any previous rule hit.')
diff --git a/src/mailman/rules/approved.py b/src/mailman/rules/approved.py
index 3e2b7bc83..3ff7d21ec 100644
--- a/src/mailman/rules/approved.py
+++ b/src/mailman/rules/approved.py
@@ -28,9 +28,9 @@ __all__ = [
import re
from email.iterators import typed_subpart_iterator
-from flufl.password import verify
-from zope.interface import implements
+from zope.interface import implementer
+from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
@@ -45,9 +45,9 @@ HEADERS = [
+@implementer(IRule)
class Approved:
"""Look for moderator pre-approval."""
- implements(IRule)
name = 'approved'
description = _('The message has a matching Approve or Approved header.')
@@ -119,8 +119,14 @@ class Approved:
else:
for header in HEADERS:
del msg[header]
- return (password is not missing and
- verify(mlist.moderator_password, password))
+ if password is missing:
+ return False
+ is_valid, new_hash = config.password_context.verify(
+ mlist.moderator_password, password)
+ if is_valid and new_hash:
+ # Hash algorithm migration.
+ mlist.moderator_password = new_hash
+ return is_valid
diff --git a/src/mailman/rules/docs/approved.rst b/src/mailman/rules/docs/approved.rst
index 9c61a7419..3f3d54455 100644
--- a/src/mailman/rules/docs/approved.rst
+++ b/src/mailman/rules/docs/approved.rst
@@ -20,9 +20,8 @@ which is shared among all the administrators.
This password will not be stored in clear text, so it must be hashed using the
configured hash protocol.
- >>> from flufl.password import lookup, make_secret
- >>> scheme = lookup(config.passwords.password_scheme.upper())
- >>> mlist.moderator_password = make_secret('super secret', scheme)
+ >>> mlist.moderator_password = config.password_context.encrypt(
+ ... 'super secret')
The ``approved`` rule determines whether the message contains the proper
approval or not.
diff --git a/src/mailman/rules/docs/header-matching.rst b/src/mailman/rules/docs/header-matching.rst
index 021974e69..20e55fadd 100644
--- a/src/mailman/rules/docs/header-matching.rst
+++ b/src/mailman/rules/docs/header-matching.rst
@@ -74,17 +74,19 @@ The header may exist and match the pattern. By default, when the header
matches, it gets held for moderator approval.
::
+ >>> from mailman.interfaces.chain import ChainEvent
>>> from mailman.testing.helpers import event_subscribers
>>> def handler(event):
- ... print event.__class__.__name__, \
- ... event.chain.name, event.msg['message-id']
+ ... if isinstance(event, ChainEvent):
+ ... print event.__class__.__name__, \
+ ... event.chain.name, event.msg['message-id']
>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '*****'
>>> msgdata = {}
>>> with event_subscribers(handler):
... process(mlist, msg, msgdata, 'header-match')
- HoldNotification hold <ant>
+ HoldEvent hold <ant>
>>> hits_and_misses(msgdata)
Rule hits:
@@ -100,7 +102,7 @@ discard such messages.
>>> with event_subscribers(handler):
... with configuration('antispam', jump_chain='discard'):
... process(mlist, msg, msgdata, 'header-match')
- DiscardNotification discard <ant>
+ DiscardEvent discard <ant>
These programmatically added headers can be removed by flushing the chain.
Now, nothing with match this message.
diff --git a/src/mailman/rules/docs/news-moderation.rst b/src/mailman/rules/docs/news-moderation.rst
index c695740fa..0400c8d9f 100644
--- a/src/mailman/rules/docs/news-moderation.rst
+++ b/src/mailman/rules/docs/news-moderation.rst
@@ -16,8 +16,8 @@ directly to the mailing list.
Set the list configuration variable to enable newsgroup moderation.
- >>> from mailman.interfaces.nntp import NewsModeration
- >>> mlist.news_moderation = NewsModeration.moderated
+ >>> from mailman.interfaces.nntp import NewsgroupModeration
+ >>> mlist.newsgroup_moderation = NewsgroupModeration.moderated
And now all messages will match the rule.
@@ -32,6 +32,6 @@ And now all messages will match the rule.
When moderation is turned off, the rule does not match.
- >>> mlist.news_moderation = NewsModeration.none
+ >>> mlist.newsgroup_moderation = NewsgroupModeration.none
>>> rule.check(mlist, msg, {})
False
diff --git a/src/mailman/rules/emergency.py b/src/mailman/rules/emergency.py
index 7b2100875..7c11b0aa3 100644
--- a/src/mailman/rules/emergency.py
+++ b/src/mailman/rules/emergency.py
@@ -17,7 +17,7 @@
"""The emergency hold rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class Emergency:
"""The emergency hold rule."""
- implements(IRule)
name = 'emergency'
diff --git a/src/mailman/rules/implicit_dest.py b/src/mailman/rules/implicit_dest.py
index 321a1775d..41d0403bc 100644
--- a/src/mailman/rules/implicit_dest.py
+++ b/src/mailman/rules/implicit_dest.py
@@ -17,7 +17,7 @@
"""The implicit destination rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
import re
from email.utils import getaddresses
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.mailinglist import IAcceptableAliasSet
@@ -35,9 +35,9 @@ from mailman.interfaces.rules import IRule
+@implementer(IRule)
class ImplicitDestination:
"""The implicit destination rule."""
- implements(IRule)
name = 'implicit-dest'
description = _('Catch messages with implicit destination.')
diff --git a/src/mailman/rules/loop.py b/src/mailman/rules/loop.py
index 9e4aa0061..9282b1705 100644
--- a/src/mailman/rules/loop.py
+++ b/src/mailman/rules/loop.py
@@ -17,7 +17,7 @@
"""Look for a posting loop."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class Loop:
"""Look for a posting loop."""
- implements(IRule)
name = 'loop'
description = _('Look for a posting loop.')
diff --git a/src/mailman/rules/max_recipients.py b/src/mailman/rules/max_recipients.py
index 79396c72f..fd09114e2 100644
--- a/src/mailman/rules/max_recipients.py
+++ b/src/mailman/rules/max_recipients.py
@@ -17,7 +17,7 @@
"""The maximum number of recipients rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -26,16 +26,16 @@ __all__ = [
from email.utils import getaddresses
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class MaximumRecipients:
"""The maximum number of recipients rule."""
- implements(IRule)
name = 'max-recipients'
description = _('Catch messages with too many explicit recipients.')
diff --git a/src/mailman/rules/max_size.py b/src/mailman/rules/max_size.py
index 420d63571..a67d87771 100644
--- a/src/mailman/rules/max_size.py
+++ b/src/mailman/rules/max_size.py
@@ -17,7 +17,7 @@
"""The maximum message size rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class MaximumSize:
"""The implicit destination rule."""
- implements(IRule)
name = 'max-size'
description = _('Catch messages that are bigger than a specified maximum.')
diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py
index cb27d89d8..b2c777dc7 100644
--- a/src/mailman/rules/moderation.py
+++ b/src/mailman/rules/moderation.py
@@ -17,7 +17,7 @@
"""Membership related rules."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -27,7 +27,7 @@ __all__ = [
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.action import Action
@@ -37,9 +37,9 @@ from mailman.interfaces.usermanager import IUserManager
+@implementer(IRule)
class MemberModeration:
"""The member moderation rule."""
- implements(IRule)
name = 'member-moderation'
description = _('Match messages sent by moderated members.')
@@ -65,9 +65,9 @@ class MemberModeration:
+@implementer(IRule)
class NonmemberModeration:
"""The nonmember moderation rule."""
- implements(IRule)
name = 'nonmember-moderation'
description = _('Match messages sent by nonmembers.')
diff --git a/src/mailman/rules/news_moderation.py b/src/mailman/rules/news_moderation.py
index 4ca9a0d8a..be0d56cb4 100644
--- a/src/mailman/rules/news_moderation.py
+++ b/src/mailman/rules/news_moderation.py
@@ -17,7 +17,7 @@
"""The news moderation rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,17 +25,17 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class ModeratedNewsgroup:
"""The news moderation rule."""
- implements(IRule)
name = 'news-moderation'
description = _(
@@ -46,4 +46,4 @@ class ModeratedNewsgroup:
def check(self, mlist, msg, msgdata):
"""See `IRule`."""
- return mlist.news_moderation == NewsModeration.moderated
+ return mlist.newsgroup_moderation == NewsgroupModeration.moderated
diff --git a/src/mailman/rules/no_subject.py b/src/mailman/rules/no_subject.py
index 1a2bab1d5..1fd0dfb8b 100644
--- a/src/mailman/rules/no_subject.py
+++ b/src/mailman/rules/no_subject.py
@@ -17,7 +17,7 @@
"""The no-Subject header rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class NoSubject:
"""The no-Subject rule."""
- implements(IRule)
name = 'no-subject'
description = _('Catch messages with no, or empty, Subject headers.')
diff --git a/src/mailman/rules/suspicious.py b/src/mailman/rules/suspicious.py
index ad1ab42cd..75fe0afab 100644
--- a/src/mailman/rules/suspicious.py
+++ b/src/mailman/rules/suspicious.py
@@ -17,7 +17,7 @@
"""The historical 'suspicious header' rule."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
import re
import logging
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
@@ -37,9 +37,9 @@ log = logging.getLogger('mailman.error')
+@implementer(IRule)
class SuspiciousHeader:
"""The historical 'suspicious header' rule."""
- implements(IRule)
name = 'suspicious-header'
description = _('Catch messages with suspicious headers.')
diff --git a/src/mailman/rules/tests/test_approved.py b/src/mailman/rules/tests/test_approved.py
index d078556ba..a1b8f99ac 100644
--- a/src/mailman/rules/tests/test_approved.py
+++ b/src/mailman/rules/tests/test_approved.py
@@ -25,17 +25,18 @@ __all__ = [
'TestApprovedNonASCII',
'TestApprovedPseudoHeader',
'TestApprovedPseudoHeaderMIME',
+ 'TestPasswordHashMigration',
]
+import os
import unittest
-from flufl.password import lookup, make_secret
-
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.rules import approved
from mailman.testing.helpers import (
+ configuration,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
@@ -48,8 +49,8 @@ class TestApproved(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- scheme = lookup(config.passwords.password_scheme.upper())
- self._mlist.moderator_password = make_secret('super secret', scheme)
+ self._mlist.moderator_password = config.password_context.encrypt(
+ 'super secret')
self._rule = approved.Approved()
self._msg = mfs("""\
From: anne@example.com
@@ -150,8 +151,8 @@ class TestApprovedPseudoHeader(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- scheme = lookup(config.passwords.password_scheme.upper())
- self._mlist.moderator_password = make_secret('super secret', scheme)
+ self._mlist.moderator_password = config.password_context.encrypt(
+ 'super secret')
self._rule = approved.Approved()
self._msg = mfs("""\
From: anne@example.com
@@ -283,8 +284,8 @@ class TestApprovedPseudoHeaderMIME(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- scheme = lookup(config.passwords.password_scheme.upper())
- self._mlist.moderator_password = make_secret('super secret', scheme)
+ self._mlist.moderator_password = config.password_context.encrypt(
+ 'super secret')
self._rule = approved.Approved()
self._msg_text_template = """\
From: anne@example.com
@@ -415,3 +416,78 @@ This is a message body with a non-ascii character =E4
# unicode errors. LP: #949924.
result = self._rule.check(self._mlist, self._msg, {})
self.assertFalse(result)
+
+
+class TestPasswordHashMigration(unittest.TestCase):
+ """Test that password hashing migrations work."""
+ # http://packages.python.org/passlib/lib/passlib.context-tutorial.html#integrating-hash-migration
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ # The default testing hash algorithm is "roundup_plaintext" which
+ # yields hashed passwords of the form: {plaintext}abc
+ #
+ # Migration is automatically supported when a more modern password
+ # hash is chosen after the original password is set. As long as the
+ # old password still validates, the migration happens automatically.
+ self._mlist.moderator_password = config.password_context.encrypt(
+ b'super secret')
+ self._rule = approved.Approved()
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: A Message with non-ascii body
+Message-ID: <ant>
+MIME-Version: 1.0
+
+A message body.
+""")
+
+ def test_valid_password_migrates(self):
+ # Now that the moderator password is set, change the default password
+ # hashing algorithm. When the old password is validated, it will be
+ # automatically migrated to the new hash.
+ self.assertEqual(self._mlist.moderator_password,
+ b'{plaintext}super secret')
+ config_file = os.path.join(config.VAR_DIR, 'passlib.config')
+ # XXX passlib seems to choose the default hashing scheme even if it is
+ # deprecated. The default scheme is either specified explicitly, or
+ # is the first in this list. This seems like a bug.
+ with open(config_file, 'w') as fp:
+ print("""\
+[passlib]
+schemes = roundup_plaintext, plaintext
+default = plaintext
+deprecated = roundup_plaintext
+""", file=fp)
+ with configuration('passwords', path=config_file):
+ self._msg['Approved'] = 'super secret'
+ result = self._rule.check(self._mlist, self._msg, {})
+ self.assertTrue(result)
+ self.assertEqual(self._mlist.moderator_password, b'super secret')
+
+ def test_invalid_password_does_not_migrate(self):
+ # Now that the moderator password is set, change the default password
+ # hashing algorithm. When the old password is invalid, it will not be
+ # automatically migrated to the new hash.
+ self.assertEqual(self._mlist.moderator_password,
+ b'{plaintext}super secret')
+ config_file = os.path.join(config.VAR_DIR, 'passlib.config')
+ # XXX passlib seems to choose the default hashing scheme even if it is
+ # deprecated. The default scheme is either specified explicitly, or
+ # is the first in this list. This seems like a bug.
+ with open(config_file, 'w') as fp:
+ print("""\
+[passlib]
+schemes = roundup_plaintext, plaintext
+default = plaintext
+deprecated = roundup_plaintext
+""", file=fp)
+ with configuration('passwords', path=config_file):
+ self._msg['Approved'] = 'not the password'
+ result = self._rule.check(self._mlist, self._msg, {})
+ self.assertFalse(result)
+ self.assertEqual(self._mlist.moderator_password,
+ b'{plaintext}super secret')
diff --git a/src/mailman/rules/truth.py b/src/mailman/rules/truth.py
index 20c152e4d..752618ced 100644
--- a/src/mailman/rules/truth.py
+++ b/src/mailman/rules/truth.py
@@ -17,7 +17,7 @@
"""A rule which always matches."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,16 +25,16 @@ __all__ = [
]
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.rules import IRule
+@implementer(IRule)
class Truth:
"""Look for any previous rule match."""
- implements(IRule)
name = 'truth'
description = _('A rule which always matches.')
diff --git a/src/mailman/runners/command.py b/src/mailman/runners/command.py
index ac611ed3a..6501474a3 100644
--- a/src/mailman/runners/command.py
+++ b/src/mailman/runners/command.py
@@ -38,7 +38,7 @@ from email.errors import HeaderParseError
from email.header import decode_header, make_header
from email.iterators import typed_subpart_iterator
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.config import config
from mailman.core.i18n import _
@@ -117,7 +117,7 @@ class CommandFinder:
continue
# Ensure that all the parts are unicodes. Since we only accept
# ASCII commands and arguments, ignore anything else.
- parts = [(part
+ parts = [(part
if isinstance(part, unicode)
else part.decode('ascii', 'ignore'))
for part in parts]
@@ -125,11 +125,10 @@ class CommandFinder:
+@implementer(IEmailResults)
class Results:
"""The email command results."""
- implements(IEmailResults)
-
def __init__(self, charset='us-ascii'):
self._output = StringIO()
self.charset = charset
diff --git a/src/mailman/runners/digest.py b/src/mailman/runners/digest.py
index c347667f8..99710dff5 100644
--- a/src/mailman/runners/digest.py
+++ b/src/mailman/runners/digest.py
@@ -201,7 +201,7 @@ class MIMEDigester(Digester):
except URLError:
log.exception(
'Digest footer decorator URI not found ({0}): {1}'.format(
- self._mlist.fqdn_listname,
+ self._mlist.fqdn_listname,
self._mlist.digest_footer_uri))
footer_text = ''
footer = MIMEText(footer_text.encode(self._charset),
@@ -281,9 +281,9 @@ class RFC1153Digester(Digester):
except URLError:
log.exception(
'Digest footer decorator URI not found ({0}): {1}'.format(
- self._mlist.fqdn_listname,
+ self._mlist.fqdn_listname,
self._mlist.digest_footer_uri))
- footer_text = ''
+ footer_text = ''
# MAS: There is no real place for the digest_footer in an RFC 1153
# compliant digest, so add it as an additional message with
# Subject: Digest Footer
diff --git a/src/mailman/runners/docs/incoming.rst b/src/mailman/runners/docs/incoming.rst
index 4a9db778e..8830031e6 100644
--- a/src/mailman/runners/docs/incoming.rst
+++ b/src/mailman/runners/docs/incoming.rst
@@ -143,9 +143,9 @@ chain will now hold all posted messages, so nothing will show up in the
pipeline queue.
::
- >>> from mailman.chains.base import ChainNotification
+ >>> from mailman.interfaces.chain import ChainEvent
>>> def on_chain(event):
- ... if isinstance(event, ChainNotification):
+ ... if isinstance(event, ChainEvent):
... print event
... print event.chain
... print 'From: {0}\nTo: {1}\nMessage-ID: {2}'.format(
@@ -158,7 +158,7 @@ pipeline queue.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
From: aperson@example.com
To: test@example.com
@@ -193,7 +193,7 @@ new chain and set it as the mailing list's start chain.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.discard.DiscardNotification ...>
+ <mailman.interfaces.chain.DiscardEvent ...>
<mailman.chains.discard.DiscardChain ...>
From: aperson@example.com
To: test@example.com
@@ -222,7 +222,7 @@ just create a new chain that does.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.reject.RejectNotification ...>
+ <mailman.interfaces.chain.RejectEvent ...>
<mailman.chains.reject.RejectChain ...>
From: aperson@example.com
To: test@example.com
diff --git a/src/mailman/runners/incoming.py b/src/mailman/runners/incoming.py
index d8db926c7..1e4ceaa65 100644
--- a/src/mailman/runners/incoming.py
+++ b/src/mailman/runners/incoming.py
@@ -26,7 +26,7 @@ prepared for delivery. Rejections, discards, and holds are processed
immediately.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -36,9 +36,9 @@ __all__ = [
from zope.component import getUtility
-from mailman.config import config
from mailman.core.chains import process
from mailman.core.runner import Runner
+from mailman.database.transaction import transaction
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
@@ -54,12 +54,12 @@ class IncomingRunner(Runner):
# Ensure that the email addresses of the message's senders are known
# to Mailman. This will be used in nonmember posting dispositions.
user_manager = getUtility(IUserManager)
- for sender in msg.senders:
- try:
- user_manager.create_address(sender)
- except ExistingAddressError:
- pass
- config.db.commit()
+ with transaction():
+ for sender in msg.senders:
+ try:
+ user_manager.create_address(sender)
+ except ExistingAddressError:
+ pass
# Process the message through the mailing list's start chain.
start_chain = (mlist.owner_chain
if msgdata.get('to_owner', False)
diff --git a/src/mailman/runners/lmtp.py b/src/mailman/runners/lmtp.py
index 45fa5a783..61db6b848 100644
--- a/src/mailman/runners/lmtp.py
+++ b/src/mailman/runners/lmtp.py
@@ -15,6 +15,9 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+# XXX This module needs to be refactored to avoid direct access to the
+# config.db global.
+
"""Mailman LMTP runner (server).
Most mail servers can be configured to deliver local messages via 'LMTP'[1].
@@ -31,6 +34,14 @@ so that the peer mail server can provide better diagnostics.
http://www.faqs.org/rfcs/rfc2033.html
"""
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'LMTPRunner',
+ ]
+
+
import email
import smtpd
import logging
@@ -41,7 +52,7 @@ from zope.component import getUtility
from mailman.config import config
from mailman.core.runner import Runner
-from mailman.database.transaction import txn
+from mailman.database.transaction import transactional
from mailman.email.message import Message
from mailman.interfaces.listmanager import IListManager
from mailman.utilities.datetime import now
@@ -80,15 +91,15 @@ SUBADDRESS_QUEUES = dict(
)
DASH = '-'
-CRLF = '\r\n'
-ERR_451 = '451 Requested action aborted: error in processing'
-ERR_501 = '501 Message has defects'
-ERR_502 = '502 Error: command HELO not implemented'
-ERR_550 = '550 Requested action not taken: mailbox unavailable'
-ERR_550_MID = '550 No Message-ID header provided'
+CRLF = b'\r\n'
+ERR_451 = b'451 Requested action aborted: error in processing'
+ERR_501 = b'501 Message has defects'
+ERR_502 = b'502 Error: command HELO not implemented'
+ERR_550 = b'550 Requested action not taken: mailbox unavailable'
+ERR_550_MID = b'550 No Message-ID header provided'
# XXX Blech
-smtpd.__version__ = 'Python LMTP runner 1.0'
+smtpd.__version__ = b'Python LMTP runner 1.0'
@@ -154,7 +165,7 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
Channel(self, conn, addr)
slog.debug('LMTP accept from %s', addr)
- @txn
+ @transactional
def process_message(self, peer, mailfrom, rcpttos, data):
try:
# Refresh the list of list names every time we process a message
@@ -228,7 +239,7 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
config.switchboards[queue].enqueue(msg, msgdata)
slog.debug('%s subaddress: %s, queue: %s',
message_id, canonical_subaddress, queue)
- status.append('250 Ok')
+ status.append(b'250 Ok')
except Exception:
slog.exception('Queue detection: %s', msg['message-id'])
config.db.abort()
diff --git a/src/mailman/runners/nntp.py b/src/mailman/runners/nntp.py
index 8339c735e..4b6cd414f 100644
--- a/src/mailman/runners/nntp.py
+++ b/src/mailman/runners/nntp.py
@@ -35,7 +35,7 @@ from cStringIO import StringIO
from mailman.config import config
from mailman.core.runner import Runner
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
COMMA = ','
COMMASPACE = ', '
@@ -106,8 +106,8 @@ def prepare_message(mlist, msg, msgdata):
# software to accept the posting, and not forward it on to the n.g.'s
# moderation address. The posting would not have gotten here if it hadn't
# already been approved. 1 == open list, mod n.g., 2 == moderated
- if mlist.news_moderation in (NewsModeration.open_moderated,
- NewsModeration.moderated):
+ if mlist.newsgroup_moderation in (NewsgroupModeration.open_moderated,
+ NewsgroupModeration.moderated):
del msg['approved']
msg['Approved'] = mlist.posting_address
# Should we restore the original, non-prefixed subject for gatewayed
@@ -116,9 +116,7 @@ def prepare_message(mlist, msg, msgdata):
# came from mailing list user.
stripped_subject = msgdata.get('stripped_subject',
msgdata.get('original_subject'))
- # XXX 2012-03-31 BAW: rename news_prefix_subject_too to nntp_. This
- # requires a schema change.
- if not mlist.news_prefix_subject_too and stripped_subject is not None:
+ if not mlist.nntp_prefix_subject_too and stripped_subject is not None:
del msg['subject']
msg['subject'] = stripped_subject
# Add the appropriate Newsgroups header. Multiple Newsgroups headers are
diff --git a/src/mailman/runners/tests/test_archiver.py b/src/mailman/runners/tests/test_archiver.py
index 6f5804cae..30cb9e461 100644
--- a/src/mailman/runners/tests/test_archiver.py
+++ b/src/mailman/runners/tests/test_archiver.py
@@ -29,7 +29,7 @@ import os
import unittest
from email import message_from_file
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.lifecycle import create_list
from mailman.config import config
@@ -44,8 +44,8 @@ from mailman.utilities.datetime import RFC822_DATE_FMT, factory, now
+@implementer(IArchiver)
class DummyArchiver:
- implements(IArchiver)
name = 'dummy'
@staticmethod
@@ -193,7 +193,7 @@ First post!
self.assertEqual(archived['x-original-date'],
'Mon, 01 Aug 2005 07:49:23 +0000')
- @configuration('archiver.dummy',
+ @configuration('archiver.dummy',
enable='yes', clobber_date='maybe', clobber_skew='1d')
def test_clobber_date_maybe_when_insane(self):
# The date is clobbered if it's farther off from now than its skew
diff --git a/src/mailman/runners/tests/test_bounce.py b/src/mailman/runners/tests/test_bounce.py
index 76eb65c1f..5c21000bf 100644
--- a/src/mailman/runners/tests/test_bounce.py
+++ b/src/mailman/runners/tests/test_bounce.py
@@ -17,17 +17,20 @@
"""Test the bounce runner."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestBounceRunner',
+ 'TestBounceRunnerBug876774',
+ 'TestStyle',
]
import unittest
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.bounces import send_probe
from mailman.app.lifecycle import create_list
@@ -235,8 +238,9 @@ Message-Id: <third>
# attributes. In particular, this will not set the bogus `bounce_processing`
# attribute which the default style set (before LP: #876774 was fixed).
+@implementer(IStyle)
class TestStyle:
- implements(IStyle)
+ """See `IStyle`."""
name = 'test'
priority = 10
@@ -249,6 +253,7 @@ class TestStyle:
styles.append(self)
+
class TestBounceRunnerBug876774(unittest.TestCase):
"""Test LP: #876774.
diff --git a/src/mailman/runners/tests/test_confirm.py b/src/mailman/runners/tests/test_confirm.py
index d2b24a2d1..62171979c 100644
--- a/src/mailman/runners/tests/test_confirm.py
+++ b/src/mailman/runners/tests/test_confirm.py
@@ -32,6 +32,7 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.registrar import IRegistrar
from mailman.interfaces.usermanager import IUserManager
from mailman.runners.command import CommandRunner
@@ -50,14 +51,14 @@ class TestConfirm(unittest.TestCase):
layer = ConfigLayer
def setUp(self):
- # Register a subscription requiring confirmation.
registrar = getUtility(IRegistrar)
- self._mlist = create_list('test@example.com')
- self._mlist.send_welcome_message = False
- self._token = registrar.register(self._mlist, 'anne@example.org')
self._commandq = config.switchboards['command']
self._runner = make_testable_runner(CommandRunner, 'command')
- config.db.commit()
+ with transaction():
+ # Register a subscription requiring confirmation.
+ self._mlist = create_list('test@example.com')
+ self._mlist.send_welcome_message = False
+ self._token = registrar.register(self._mlist, 'anne@example.org')
def test_confirm_with_re_prefix(self):
subject = 'Re: confirm {0}'.format(self._token)
diff --git a/src/mailman/runners/tests/test_lmtp.py b/src/mailman/runners/tests/test_lmtp.py
index 87b69c7e4..46d4ed986 100644
--- a/src/mailman/runners/tests/test_lmtp.py
+++ b/src/mailman/runners/tests/test_lmtp.py
@@ -31,7 +31,7 @@ import unittest
from datetime import datetime
from mailman.app.lifecycle import create_list
-from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.testing.helpers import get_lmtp_client, get_queue_messages
from mailman.testing.layers import LMTPLayer
@@ -43,8 +43,8 @@ class TestLMTP(unittest.TestCase):
layer = LMTPLayer
def setUp(self):
- self._mlist = create_list('test@example.com')
- config.db.commit()
+ with transaction():
+ self._mlist = create_list('test@example.com')
self._lmtp = get_lmtp_client(quiet=True)
self._lmtp.lhlo('remote.example.org')
diff --git a/src/mailman/runners/tests/test_nntp.py b/src/mailman/runners/tests/test_nntp.py
index 426e829d8..477bccfa3 100644
--- a/src/mailman/runners/tests/test_nntp.py
+++ b/src/mailman/runners/tests/test_nntp.py
@@ -33,7 +33,7 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.config import config
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.runners import nntp
from mailman.testing.helpers import (
LogFileMark,
@@ -67,7 +67,7 @@ Testing
# Approved header, which NNTP software uses to forward to the
# newsgroup. The message would not have gotten to the mailing list if
# it wasn't already approved.
- self._mlist.news_moderation = NewsModeration.moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.moderated
nntp.prepare_message(self._mlist, self._msg, {})
self.assertEqual(self._msg['approved'], 'test@example.com')
@@ -76,14 +76,14 @@ Testing
# message will get an Approved header, which NNTP software uses to
# forward to the newsgroup. The message would not have gotten to the
# mailing list if it wasn't already approved.
- self._mlist.news_moderation = NewsModeration.open_moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.open_moderated
nntp.prepare_message(self._mlist, self._msg, {})
self.assertEqual(self._msg['approved'], 'test@example.com')
def test_moderation_removes_previous_approved_header(self):
# Any existing Approved header is removed from moderated messages.
self._msg['Approved'] = 'a bogus approval'
- self._mlist.news_moderation = NewsModeration.moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.moderated
nntp.prepare_message(self._mlist, self._msg, {})
headers = self._msg.get_all('approved')
self.assertEqual(len(headers), 1)
@@ -92,7 +92,7 @@ Testing
def test_open_moderation_removes_previous_approved_header(self):
# Any existing Approved header is removed from moderated messages.
self._msg['Approved'] = 'a bogus approval'
- self._mlist.news_moderation = NewsModeration.open_moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.open_moderated
nntp.prepare_message(self._mlist, self._msg, {})
headers = self._msg.get_all('approved')
self.assertEqual(len(headers), 1)
@@ -102,7 +102,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = False
+ self._mlist.nntp_prefix_subject_too = False
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(stripped_subject='Your test')
@@ -115,7 +115,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = False
+ self._mlist.nntp_prefix_subject_too = False
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(original_subject='Your test')
@@ -128,7 +128,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = True
+ self._mlist.nntp_prefix_subject_too = True
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(stripped_subject='Your test')
@@ -141,7 +141,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = True
+ self._mlist.nntp_prefix_subject_too = True
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(original_subject='Your test')
diff --git a/src/mailman/runners/tests/test_owner.py b/src/mailman/runners/tests/test_owner.py
index 622bb2255..4ea5771be 100644
--- a/src/mailman/runners/tests/test_owner.py
+++ b/src/mailman/runners/tests/test_owner.py
@@ -37,6 +37,7 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.interfaces.member import MemberRole
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
@@ -59,17 +60,17 @@ class TestEmailToOwner(unittest.TestCase):
self._mlist = create_list('test@example.com')
# Add some owners, moderators, and members
manager = getUtility(IUserManager)
- anne = manager.create_address('anne@example.com')
- bart = manager.create_address('bart@example.com')
- cris = manager.create_address('cris@example.com')
- dave = manager.create_address('dave@example.com')
- self._mlist.subscribe(anne, MemberRole.member)
- self._mlist.subscribe(anne, MemberRole.owner)
- self._mlist.subscribe(bart, MemberRole.moderator)
- self._mlist.subscribe(bart, MemberRole.owner)
- self._mlist.subscribe(cris, MemberRole.moderator)
- self._mlist.subscribe(dave, MemberRole.member)
- config.db.commit()
+ with transaction():
+ anne = manager.create_address('anne@example.com')
+ bart = manager.create_address('bart@example.com')
+ cris = manager.create_address('cris@example.com')
+ dave = manager.create_address('dave@example.com')
+ self._mlist.subscribe(anne, MemberRole.member)
+ self._mlist.subscribe(anne, MemberRole.owner)
+ self._mlist.subscribe(bart, MemberRole.moderator)
+ self._mlist.subscribe(bart, MemberRole.owner)
+ self._mlist.subscribe(cris, MemberRole.moderator)
+ self._mlist.subscribe(dave, MemberRole.member)
self._inq = make_testable_runner(IncomingRunner, 'in')
self._pipelineq = make_testable_runner(PipelineRunner, 'pipeline')
self._outq = make_testable_runner(OutgoingRunner, 'out')
diff --git a/src/mailman/runners/tests/test_pipeline.py b/src/mailman/runners/tests/test_pipeline.py
index 8776bf844..3f8f62ab0 100644
--- a/src/mailman/runners/tests/test_pipeline.py
+++ b/src/mailman/runners/tests/test_pipeline.py
@@ -27,7 +27,7 @@ __all__ = [
import unittest
-from zope.interface import implements
+from zope.interface import implementer
from mailman.app.lifecycle import create_list
from mailman.config import config
@@ -41,8 +41,10 @@ from mailman.testing.layers import ConfigLayer
+@implementer(IHandler)
class MyTestHandler:
- implements(IHandler)
+ """See `IHandler`."""
+
name = 'test handler'
description = 'A test handler'
@@ -54,8 +56,9 @@ class MyTestHandler:
self._test.mark(self._marker)
+
+@implementer(IPipeline)
class MyTestPipeline:
- implements(IPipeline)
name = 'test'
description = 'a test pipeline'
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index b6900dca6..cb4da396d 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -17,7 +17,7 @@
"""Application of list styles to new and existing lists."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -28,24 +28,24 @@ __all__ = [
# XXX Styles need to be reconciled with lazr.config.
from datetime import timedelta
-from zope.interface import implements
+from zope.interface import implementer
from mailman.core.i18n import _
from mailman.interfaces.action import Action, FilterAction
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.styles import IStyle
+@implementer(IStyle)
class DefaultStyle:
"""The default (i.e. legacy) style."""
- implements(IStyle)
-
name = 'default'
priority = 0 # the lowest priority style
@@ -55,9 +55,8 @@ class DefaultStyle:
mlist = mailing_list
# List identity.
mlist.display_name = mlist.list_name.capitalize()
- mlist.list_id = '{0.list_name}.{0.mail_host}'.format(mlist)
mlist.include_rfc2369_headers = True
- mlist.include_list_post_header = True
+ mlist.allow_list_posts = True
# Most of these were ripped from the old MailList.InitVars() method.
mlist.volume = 1
mlist.post_id = 1
@@ -117,9 +116,7 @@ from: .*@uplinkpro.com
mlist.default_member_action = Action.defer
mlist.default_nonmember_action = Action.hold
# Archiver
- mlist.archive = True
- mlist.archive_private = 0
- mlist.archive_volume_frequency = 1
+ mlist.archive_policy = ArchivePolicy.public
mlist.emergency = False
mlist.member_moderation_notice = ''
mlist.accept_these_nonmembers = []
@@ -127,7 +124,6 @@ from: .*@uplinkpro.com
mlist.reject_these_nonmembers = []
mlist.discard_these_nonmembers = []
mlist.forward_auto_discards = True
- mlist.generic_nonmember_action = 1
mlist.nonmember_rejection_notice = ''
# Max autoresponses per day. A mapping between addresses and a
# 2-tuple of the date of the last autoresponse and the number of
@@ -175,10 +171,10 @@ from: .*@uplinkpro.com
mlist.linked_newsgroup = ''
mlist.gateway_to_news = False
mlist.gateway_to_mail = False
- mlist.news_prefix_subject_too = True
+ mlist.nntp_prefix_subject_too = True
# In patch #401270, this was called newsgroup_is_moderated, but the
# semantics weren't quite the same.
- mlist.news_moderation = NewsModeration.none
+ mlist.newsgroup_moderation = NewsgroupModeration.none
# Topics
#
# `topics' is a list of 4-tuples of the following form:
diff --git a/src/mailman/styles/docs/styles.rst b/src/mailman/styles/docs/styles.rst
index 90a02227b..8f589f10b 100644
--- a/src/mailman/styles/docs/styles.rst
+++ b/src/mailman/styles/docs/styles.rst
@@ -62,10 +62,10 @@ Registering styles
New styles must implement the ``IStyle`` interface.
- >>> from zope.interface import implements
+ >>> from zope.interface import implementer
>>> from mailman.interfaces.styles import IStyle
- >>> class TestStyle:
- ... implements(IStyle)
+ >>> @implementer(IStyle)
+ ... class TestStyle:
... name = 'test'
... priority = 10
... def apply(self, mailing_list):
diff --git a/src/mailman/styles/manager.py b/src/mailman/styles/manager.py
index f1d3f1dfb..c2729abfb 100644
--- a/src/mailman/styles/manager.py
+++ b/src/mailman/styles/manager.py
@@ -17,29 +17,31 @@
"""Style manager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'StyleManager',
+ 'handle_ConfigurationUpdatedEvent',
]
from operator import attrgetter
-from zope.interface import implements
+from zope.component import getUtility
+from zope.interface import implementer
from zope.interface.verify import verifyObject
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
from mailman.utilities.modules import call_name
+@implementer(IStyleManager)
class StyleManager:
"""The built-in style manager."""
- implements(IStyleManager)
-
def __init__(self):
"""Install all styles from the configuration files."""
self._styles = {}
@@ -89,3 +91,9 @@ class StyleManager:
"""See `IStyleManager`."""
# Let KeyErrors percolate up.
del self._styles[style.name]
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ getUtility(IStyleManager).populate()
diff --git a/src/mailman/styles/tests/test_styles.py b/src/mailman/styles/tests/test_styles.py
index ce8b5064d..990ce541f 100644
--- a/src/mailman/styles/tests/test_styles.py
+++ b/src/mailman/styles/tests/test_styles.py
@@ -28,7 +28,7 @@ __all__ = [
import unittest
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import implementer
from zope.interface.exceptions import DoesNotImplement
from mailman.interfaces.styles import (
@@ -37,8 +37,8 @@ from mailman.testing.layers import ConfigLayer
+@implementer(IStyle)
class DummyStyle:
- implements(IStyle)
name = 'dummy'
priority = 1
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 3648a6710..054dd4ff7 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -17,7 +17,7 @@
"""Various test helpers."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -25,6 +25,7 @@ __all__ = [
'TestableMaster',
'body_line_iterator',
'call_api',
+ 'chdir',
'configuration',
'digest_mbox',
'event_subscribers',
@@ -35,6 +36,7 @@ __all__ = [
'reset_the_world',
'specialized_message_from_string',
'subscribe',
+ 'temporary_db',
'wait_for_webservice',
]
@@ -64,6 +66,7 @@ from zope.component import getUtility
from mailman.bin.master import Loop as Master
from mailman.config import config
+from mailman.database.transaction import transaction
from mailman.email.message import Message
from mailman.interfaces.member import MemberRole
from mailman.interfaces.messages import IMessageStore
@@ -237,13 +240,14 @@ def get_lmtp_client(quiet=False):
# It's possible the process has started but is not yet accepting
# connections. Wait a little while.
lmtp = LMTP()
+ #lmtp.debuglevel = 1
until = datetime.datetime.now() + as_timedelta(config.devmode.wait)
while datetime.datetime.now() < until:
try:
response = lmtp.connect(
config.mta.lmtp_host, int(config.mta.lmtp_port))
if not quiet:
- print response
+ print(response)
return lmtp
except socket.error as error:
if error[0] == errno.ECONNREFUSED:
@@ -341,14 +345,14 @@ def call_api(url, data=None, method=None, username=None, password=None):
@contextmanager
def event_subscribers(*subscribers):
- """Temporarily set the Zope event subscribers list.
+ """Temporarily extend the Zope event subscribers list.
:param subscribers: A sequence of event subscribers.
:type subscribers: sequence of callables, each receiving one argument, the
event.
"""
old_subscribers = event.subscribers[:]
- event.subscribers = list(subscribers)
+ event.subscribers.extend(subscribers)
try:
yield
finally:
@@ -361,8 +365,14 @@ class configuration:
def __init__(self, section, **kws):
self._section = section
+ # Most tests don't care about the name given to the temporary
+ # configuration. Usually we'll just craft a random one, but some
+ # tests do care, so give them a hook to set it.
+ if '_configname' in kws:
+ self._uuid = kws.pop('_configname')
+ else:
+ self._uuid = uuid.uuid4().hex
self._values = kws.copy()
- self._uuid = uuid.uuid4().hex
def _apply(self):
lines = ['[{0}]'.format(self._section)]
@@ -392,24 +402,52 @@ class configuration:
+@contextmanager
+def temporary_db(db):
+ real_db = config.db
+ config.db = db
+ try:
+ yield
+ finally:
+ config.db = real_db
+
+
+
+class chdir:
+ """A context manager for temporary directory changing."""
+ def __init__(self, directory):
+ self._curdir = None
+ self._directory = directory
+
+ def __enter__(self):
+ self._curdir = os.getcwd()
+ os.chdir(self._directory)
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ os.chdir(self._curdir)
+ # Don't suppress exceptions.
+ return False
+
+
+
def subscribe(mlist, first_name, role=MemberRole.member):
"""Helper for subscribing a sample person to a mailing list."""
user_manager = getUtility(IUserManager)
email = '{0}person@example.com'.format(first_name[0].lower())
full_name = '{0} Person'.format(first_name)
- person = user_manager.get_user(email)
- if person is None:
- address = user_manager.get_address(email)
- if address is None:
- person = user_manager.create_user(email, full_name)
+ with transaction():
+ person = user_manager.get_user(email)
+ if person is None:
+ address = user_manager.get_address(email)
+ if address is None:
+ person = user_manager.create_user(email, full_name)
+ preferred_address = list(person.addresses)[0]
+ mlist.subscribe(preferred_address, role)
+ else:
+ mlist.subscribe(address, role)
+ else:
preferred_address = list(person.addresses)[0]
mlist.subscribe(preferred_address, role)
- else:
- mlist.subscribe(address, role)
- else:
- preferred_address = list(person.addresses)[0]
- mlist.subscribe(preferred_address, role)
- config.db.commit()
@@ -437,9 +475,9 @@ def reset_the_world():
os.remove(os.path.join(dirpath, filename))
# Clear out messages in the message store.
message_store = getUtility(IMessageStore)
- for message in message_store.messages:
- message_store.delete_message(message['message-id'])
- config.db.commit()
+ with transaction():
+ for message in message_store.messages:
+ message_store.delete_message(message['message-id'])
# Reset the global style manager.
getUtility(IStyleManager).populate()
# Remove all dynamic header-match rules.
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 41ef86935..3a3e1f684 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -25,7 +25,7 @@
# eventually get rid of the zope.test* dependencies and use something like
# testresources or some such.
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -56,6 +56,7 @@ from mailman.config import config
from mailman.core import initialize
from mailman.core.initialize import INHIBIT_CONFIG_FILE
from mailman.core.logging import get_handler
+from mailman.database.transaction import transaction
from mailman.interfaces.domain import IDomainManager
from mailman.testing.helpers import (
TestableMaster, get_lmtp_client, reset_the_world)
@@ -116,8 +117,6 @@ class ConfigLayer(MockAndMonkeyLayer):
test_config = dedent("""
[mailman]
layout: testing
- [passwords]
- password_scheme: cleartext
[paths.testing]
var_dir: %s
[devmode]
@@ -128,7 +127,7 @@ class ConfigLayer(MockAndMonkeyLayer):
config.create_paths = True
config.push('test config', test_config)
# Initialize everything else.
- initialize.initialize_2()
+ initialize.initialize_2(testing=True)
initialize.initialize_3()
# When stderr debugging is enabled, subprocess root loggers should
# also be more verbose.
@@ -176,7 +175,7 @@ class ConfigLayer(MockAndMonkeyLayer):
config_file = os.path.join(cls.var_dir, 'test.cfg')
with open(config_file, 'w') as fp:
fp.write(test_config)
- print >> fp
+ print(file=fp)
config.filename = config_file
@classmethod
@@ -189,10 +188,10 @@ class ConfigLayer(MockAndMonkeyLayer):
@classmethod
def testSetUp(cls):
# Add an example domain.
- getUtility(IDomainManager).add(
- 'example.com', 'An example domain.',
- 'http://lists.example.com', 'postmaster@example.com')
- config.db.commit()
+ with transaction():
+ getUtility(IDomainManager).add(
+ 'example.com', 'An example domain.',
+ 'http://lists.example.com', 'postmaster@example.com')
@classmethod
def testTearDown(cls):
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
index 4699cb882..bba450352 100644
--- a/src/mailman/testing/mta.py
+++ b/src/mailman/testing/mta.py
@@ -17,7 +17,7 @@
"""Fake MTA for testing purposes."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,7 +31,7 @@ from Queue import Empty, Queue
from lazr.smtptest.controller import QueueController
from lazr.smtptest.server import Channel, QueueServer
-from zope.interface import implements
+from zope.interface import implementer
from mailman.interfaces.mta import IMailTransportAgentLifecycle
@@ -40,11 +40,10 @@ log = logging.getLogger('lazr.smtptest')
+@implementer(IMailTransportAgentLifecycle)
class FakeMTA:
"""Fake MTA for testing purposes."""
- implements(IMailTransportAgentLifecycle)
-
def create(self, mlist):
pass
diff --git a/src/mailman/testing/passlib.cfg b/src/mailman/testing/passlib.cfg
new file mode 100644
index 000000000..225ecd49b
--- /dev/null
+++ b/src/mailman/testing/passlib.cfg
@@ -0,0 +1,4 @@
+[passlib]
+# Use a predictable hashing algorithm with plain text and no salt. This is
+# *only* useful for debugging and unit testing.
+schemes = roundup_plaintext
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index 91613cc8d..141d74a8f 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -18,9 +18,9 @@
# A testing configuration.
# For testing against PostgreSQL.
-#[database]
-#class: mailman.database.postgresql.PostgreSQLDatabase
-#url: postgres://barry:barry@localhost/mailman
+# [database]
+# class: mailman.database.postgresql.PostgreSQLDatabase
+# url: postgres://barry:barry@localhost/mailman
[mailman]
site_owner: noreply@example.com
@@ -30,6 +30,9 @@ smtp_port: 9025
lmtp_port: 9024
incoming: mailman.testing.mta.FakeMTA
+[passwords]
+path: python:mailman.testing.passlib
+
[webservice]
port: 9001
diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py
index a2c1ab592..b769f07d6 100644
--- a/src/mailman/tests/test_documentation.py
+++ b/src/mailman/tests/test_documentation.py
@@ -34,11 +34,14 @@ import sys
import doctest
import unittest
+from inspect import isfunction, ismethod
+
import mailman
from mailman.app.lifecycle import create_list
from mailman.config import config
-from mailman.testing.helpers import call_api, specialized_message_from_string
+from mailman.testing.helpers import (
+ call_api, chdir, specialized_message_from_string)
from mailman.testing.layers import SMTPLayer
@@ -46,23 +49,6 @@ DOT = '.'
-class chdir:
- """A context manager for temporary directory changing."""
- def __init__(self, directory):
- self._curdir = None
- self._directory = directory
-
- def __enter__(self):
- self._curdir = os.getcwd()
- os.chdir(self._directory)
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- os.chdir(self._curdir)
- # Don't suppress exceptions.
- return False
-
-
-
def stop():
"""Call into pdb.set_trace()"""
# Do the import here so that you get the wacky special hacked pdb instead
@@ -185,7 +171,10 @@ def setup(testobj):
def teardown(testobj):
for cleanup in testobj.globs['cleanups']:
- cleanup()
+ if isfunction(cleanup) or ismethod(cleanup):
+ cleanup()
+ else:
+ cleanup[0](*cleanup[1:])
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index f77d86e9a..6cdba0de3 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -17,7 +17,7 @@
"""Importer routines."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,7 +31,7 @@ import datetime
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
@@ -47,7 +47,7 @@ TYPES = dict(
bounce_info_stale_after=seconds_to_delta,
bounce_you_are_disabled_warnings_interval=seconds_to_delta,
digest_volume_frequency=DigestFrequency,
- news_moderation=NewsModeration,
+ newsgroup_moderation=NewsgroupModeration,
personalize=Personalization,
reply_goes_to_list=ReplyToMunging,
)
@@ -56,6 +56,7 @@ TYPES = dict(
# Attribute names in Mailman 2 which are renamed in Mailman 3.
NAME_MAPPINGS = dict(
host_name='mail_host',
+ include_list_post_header='allow_list_posts',
real_name='display_name',
)
@@ -85,5 +86,5 @@ def import_config_pck(mlist, config_dict):
try:
setattr(mlist, key, value)
except TypeError:
- print >> sys.stderr, 'Type conversion error:', key
+ print('Type conversion error:', key, file=sys.stderr)
raise
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
new file mode 100644
index 000000000..95c85c47a
--- /dev/null
+++ b/src/mailman/utilities/passwords.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""A wrapper around passlib."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'handle_ConfigurationUpdatedEvent',
+ ]
+
+
+
+from passlib.context import CryptContext
+from pkg_resources import resource_string
+
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+
+
+
+class PasswordContext:
+ def __init__(self, config):
+ # Is the context coming from a file system or Python path?
+ if config.passwords.path.startswith('python:'):
+ resource_path = config.passwords.path[7:]
+ package, dot, resource = resource_path.rpartition('.')
+ config_string = resource_string(package, resource + '.cfg')
+ else:
+ with open(config.passwords.path, 'rb') as fp:
+ config_string = fp.read()
+ self._context = CryptContext.from_string(config_string)
+
+ def encrypt(self, secret):
+ return self._context.encrypt(secret)
+
+ def verify(self, hashed, password):
+ # Support hash algorithm migration. Yes, the order of arguments is
+ # reversed, for backward compatibility with flufl.password. XXX fix
+ # this eventually.
+ return self._context.verify_and_update(password, hashed)
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ # Just reset the password context.
+ event.config.password_context = PasswordContext(event.config)
diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py
index 58a51e61b..396da4aa8 100644
--- a/src/mailman/utilities/tests/test_import.py
+++ b/src/mailman/utilities/tests/test_import.py
@@ -17,10 +17,11 @@
"""Tests for config.pck imports."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestBasicImport',
]
@@ -44,7 +45,7 @@ class TestBasicImport(unittest.TestCase):
self._pckdict = cPickle.load(fp)
def tearDown(self):
- remove_list(self._mlist.fqdn_listname, self._mlist)
+ remove_list(self._mlist)
def _import(self):
import_config_pck(self._mlist, self._pckdict)
@@ -62,8 +63,8 @@ class TestBasicImport(unittest.TestCase):
self.assertEqual(self._mlist.mail_host, 'heresy.example.org')
def test_rfc2369_headers(self):
- self._mlist.include_list_post_header = False
+ self._mlist.allow_list_posts = False
self._mlist.include_rfc2369_headers = False
self._import()
- self.assertTrue(self._mlist.include_list_post_header)
+ self.assertTrue(self._mlist.allow_list_posts)
self.assertTrue(self._mlist.include_rfc2369_headers)
diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py
new file mode 100644
index 000000000..7b2931855
--- /dev/null
+++ b/src/mailman/utilities/tests/test_passwords.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Testing the password utility."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestPasswords',
+ ]
+
+
+import os
+import unittest
+
+from mailman.config import config
+from mailman.testing.helpers import configuration
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestPasswords(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_default_passlib(self):
+ # By default, testing uses the roundup_plaintext hash algorithm, which
+ # is just plaintext with a prefix.
+ self.assertEqual(config.password_context.encrypt('my password'),
+ '{plaintext}my password')
+
+ def test_passlib_from_file_path(self):
+ # Set up this test to use a passlib configuration file specified with
+ # a file system path. We prove we're using the new configuration
+ # because a non-prefixed, i.e. non-roundup, plaintext hash algorithm
+ # will be used. When a file system path is used, the file can end in
+ # any suffix.
+ config_file = os.path.join(config.VAR_DIR, 'passlib.config')
+ with open(config_file, 'w') as fp:
+ print("""\
+[passlib]
+schemes = plaintext
+""", file=fp)
+ with configuration('passwords', path=config_file):
+ self.assertEqual(config.password_context.encrypt('my password'),
+ 'my password')