diff options
| -rwxr-xr-x | copybump.py | 3 | ||||
| -rw-r--r-- | src/mailman/app/moderator.py | 13 | ||||
| -rw-r--r-- | src/mailman/app/registrar.py | 2 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_moderation.py | 106 | ||||
| -rw-r--r-- | src/mailman/bin/master.py | 1 | ||||
| -rw-r--r-- | src/mailman/chains/moderation.py | 2 | ||||
| -rw-r--r-- | src/mailman/core/pipelines.py | 7 | ||||
| -rw-r--r-- | src/mailman/database/mailman.sql | 2 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 5 | ||||
| -rw-r--r-- | src/mailman/interfaces/domain.py | 38 | ||||
| -rw-r--r-- | src/mailman/model/docs/domains.rst (renamed from src/mailman/model/docs/domains.txt) | 6 | ||||
| -rw-r--r-- | src/mailman/model/docs/requests.rst (renamed from src/mailman/model/docs/requests.txt) | 2 | ||||
| -rw-r--r-- | src/mailman/model/domain.py | 52 | ||||
| -rw-r--r-- | src/mailman/rest/docs/domains.rst (renamed from src/mailman/rest/docs/domains.txt) | 18 | ||||
| -rw-r--r-- | src/mailman/rest/domains.py | 8 | ||||
| -rw-r--r-- | src/mailman/runners/outgoing.py | 3 |
16 files changed, 198 insertions, 70 deletions
diff --git a/copybump.py b/copybump.py index 2f78c79b5..87fa5dfb9 100755 --- a/copybump.py +++ b/copybump.py @@ -27,6 +27,9 @@ def do_file(path, owner): start = (mo.group('end') if mo.group('start') is None else mo.group('start')) + if int(start) == this_year: + out_file.write(line) + continue print('# Copyright (C) {}-{} {}'.format( start, this_year, owner), file=out_file) for line in in_file: diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py index 0ba6f492a..d2c6600ad 100644 --- a/src/mailman/app/moderator.py +++ b/src/mailman/app/moderator.py @@ -30,9 +30,10 @@ __all__ = [ 'hold_unsubscription', ] + +import time import logging -from datetime import datetime from email.utils import formataddr, formatdate, getaddresses, make_msgid from zope.component import getUtility @@ -49,6 +50,7 @@ from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, NotAMemberError) from mailman.interfaces.messages import IMessageStore from mailman.interfaces.requests import IRequests, RequestType +from mailman.utilities.datetime import now from mailman.utilities.i18n import make @@ -96,7 +98,7 @@ def hold_message(mlist, msg, msgdata=None, reason=None): msgdata['_mod_sender'] = msg.sender msgdata['_mod_subject'] = msg.get('subject', _('(no subject)')) msgdata['_mod_reason'] = reason - msgdata['_mod_hold_date'] = datetime.now().isoformat() + msgdata['_mod_hold_date'] = now().isoformat() # Now hold this request. We'll use the message_id as the key. requestsdb = getUtility(IRequests).get_list_requests(mlist) request_id = requestsdb.hold_request( @@ -146,12 +148,13 @@ def handle_message(mlist, id, action, # Queue the file for delivery. Trying to deliver the message directly # here can lead to a huge delay in web turnaround. Log the moderation # and add a header. - msg['X-Mailman-Approved-At'] = formatdate(localtime=True) + msg['X-Mailman-Approved-At'] = formatdate( + time.mktime(now().timetuple()), localtime=True) vlog.info('held message approved, message-id: %s', msg.get('message-id', 'n/a')) # Stick the message back in the incoming queue for further # processing. - config.switchboards['in'].enqueue(msg, _metadata=msgdata) + config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata) else: raise AssertionError('Unexpected action: {0}'.format(action)) # Forward the message. @@ -195,7 +198,7 @@ def handle_message(mlist, id, action, def hold_subscription(mlist, address, realname, password, mode, language): - data = dict(when=datetime.now().isoformat(), + data = dict(when=now().isoformat(), address=address, realname=realname, password=password, diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py index f6f2e8679..42111ef53 100644 --- a/src/mailman/app/registrar.py +++ b/src/mailman/app/registrar.py @@ -75,7 +75,7 @@ class Registrar: # For i18n interpolation. confirm_url = mlist.domain.confirm_url(token) email_address = email - domain_name = mlist.domain.email_host + domain_name = mlist.domain.mail_host contact_address = mlist.domain.contact_address # Send a verification email to the address. text = _(resource_string('mailman.templates.en', 'verify.txt')) diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py new file mode 100644 index 000000000..59e9f3643 --- /dev/null +++ b/src/mailman/app/tests/test_moderation.py @@ -0,0 +1,106 @@ +# Copyright (C) 2011 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/>. + +"""Moderation tests.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.app.moderator import handle_message, hold_message +from mailman.interfaces.action import Action +from mailman.runners.incoming import IncomingRunner +from mailman.runners.outgoing import OutgoingRunner +from mailman.runners.pipeline import PipelineRunner +from mailman.testing.helpers import ( + make_testable_runner, specialized_message_from_string) +from mailman.testing.layers import SMTPLayer + + + +class TestModeration(unittest.TestCase): + """Test moderation functionality.""" + + layer = SMTPLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + self._msg = specialized_message_from_string("""\ +From: anne@example.com +To: test@example.com +Subject: hold me +Message-ID: <alpha> + +""") + self._in = make_testable_runner(IncomingRunner, 'in') + self._pipeline = make_testable_runner(PipelineRunner, 'pipeline') + self._out = make_testable_runner(OutgoingRunner, 'out') + + def test_accepted_message_gets_posted(self): + # A message that is accepted by the moderator should get posted to the + # mailing list. LP: #827697 + msgdata = dict(listname='test@example.com', + recipients=['bart@example.com']) + request_id = hold_message(self._mlist, self._msg, msgdata) + handle_message(self._mlist, request_id, Action.accept) + self._in.run() + self._pipeline.run() + self._out.run() + messages = list(SMTPLayer.smtpd.messages) + self.assertEqual(len(messages), 1) + message = messages[0] + # Delete variable headers which can't be compared. + self.assertTrue('x-mailman-version' in message) + del message['x-mailman-version'] + self.assertTrue('x-peer' in message) + del message['x-peer'] + self.assertEqual(message.as_string(), """\ +From: anne@example.com +To: test@example.com +Message-ID: <alpha> +X-Mailman-Approved-At: Mon, 01 Aug 2005 07:49:23 -0400 +Subject: [Test] hold me +X-BeenThere: test@example.com +Precedence: list +List-Id: <test.example.com> +X-Message-ID-Hash: XZ3DGG4V37BZTTLXNUX4NABB4DNQHTCP +List-Post: <mailto:test@example.com> +List-Subscribe: <http://lists.example.com/listinfo/test@example.com>, + <mailto:test-join@example.com> +Archived-At: http://lists.example.com/archives/XZ3DGG4V37BZTTLXNUX4NABB4DNQHTCP +List-Unsubscribe: <http://lists.example.com/listinfo/test@example.com>, + <mailto:test-leave@example.com> +List-Archive: <http://lists.example.com/archives/test@example.com> +List-Help: <mailto:test-request@example.com?subject=help> +X-MailFrom: test-bounces@example.com +X-RcptTo: bart@example.com + +""") + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestModeration)) + return suite diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index d982b385f..d910b491d 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'Loop', 'main', ] diff --git a/src/mailman/chains/moderation.py b/src/mailman/chains/moderation.py index d6104fd66..fcba31f82 100644 --- a/src/mailman/chains/moderation.py +++ b/src/mailman/chains/moderation.py @@ -71,7 +71,7 @@ class ModerationChain: # moderation.py rule for details. This is stored in the metadata as a # string so that it can be stored in the pending table. action = Action[msgdata.get('moderation_action')] - # defer and accept are not valid moderation actions. + # defer is not a valid moderation action. jump_chain = { Action.accept: 'accept', Action.discard: 'discard', diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py index 15adca501..7efc8e329 100644 --- a/src/mailman/core/pipelines.py +++ b/src/mailman/core/pipelines.py @@ -26,6 +26,8 @@ __all__ = [ ] +import logging + from zope.interface import implements from zope.interface.verify import verifyObject @@ -35,6 +37,8 @@ from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler from mailman.interfaces.pipeline import IPipeline +log = logging.getLogger('mailman.debug') + def process(mlist, msg, msgdata, pipeline_name='built-in'): @@ -45,8 +49,11 @@ def process(mlist, msg, msgdata, pipeline_name='built-in'): :param msgdata: The message metadata dictionary. :param pipeline_name: The name of the pipeline to process through. """ + message_id = msg.get('message-id', 'n/a') pipeline = config.pipelines[pipeline_name] for handler in pipeline: + log.debug('[pipeline] processing {0}: {1}'.format( + handler.name, message_id)) handler.process(mlist, msg, msgdata) diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index c6f63c6e5..0a773c28e 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -79,7 +79,7 @@ CREATE INDEX ix_contentfilter_mailing_list_id CREATE TABLE domain ( id INTEGER NOT NULL, - email_host TEXT, + mail_host TEXT, base_url TEXT, description TEXT, contact_address TEXT, diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index c2fa66905..a0665171d 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -28,6 +28,7 @@ Architecture * Using the above events, when a mailing list is deleted, all its members are deleted, as well as all held message requests (but not the held messages themselves). (LP: 827036) + * IDomain.email_host -> .mail_host (LP: #831660) REST ---- @@ -64,6 +65,10 @@ Testing * Handle SIGTERM in the REST server so that the test suite always shuts down correctly. (LP: #770328) +Other bugs +---------- + * Moderating a message with Action.accept now sends the message. (LP: #827697) + 3.0 alpha 7 -- "Mission" ======================== diff --git a/src/mailman/interfaces/domain.py b/src/mailman/interfaces/domain.py index e36ad03d3..baf8dafcb 100644 --- a/src/mailman/interfaces/domain.py +++ b/src/mailman/interfaces/domain.py @@ -45,7 +45,7 @@ class BadDomainSpecificationError(MailmanError): class IDomain(Interface): """Interface representing domains.""" - email_host = Attribute('The host name for email for this domain.') + mail_host = Attribute('The host name for email for this domain.') url_host = Attribute( 'The host name for the web interface for this domain.') @@ -80,50 +80,50 @@ class IDomain(Interface): class IDomainManager(Interface): """The manager of domains.""" - def add(email_host, description=None, base_url=None, contact_address=None): + def add(mail_host, description=None, base_url=None, contact_address=None): """Add a new domain. - :param email_host: The email host name for the domain. - :type email_host: string + :param mail_host: The email host name for the domain. + :type mail_host: string :param description: The description of the domain. :type description: string :param base_url: The base url, including the scheme for the web interface of the domain. If not given, it defaults to - http://`email_host`/ + http://`mail_host`/ :type base_url: string :param contact_address: The email contact address for the human managing the domain. If not given, defaults to - postmaster@`email_host` + postmaster@`mail_host` :type contact_address: string :return: The new domain object :rtype: `IDomain` - :raises `BadDomainSpecificationError`: when the `email_host` is + :raises `BadDomainSpecificationError`: when the `mail_host` is already registered. """ - def remove(email_host): + def remove(mail_host): """Remove the domain. - :param email_host: The email host name of the domain to remove. - :type email_host: string + :param mail_host: The email host name of the domain to remove. + :type mail_host: string :raises KeyError: if the named domain does not exist. """ - def __getitem__(email_host): + def __getitem__(mail_host): """Return the named domain. - :param email_host: The email host name of the domain to remove. - :type email_host: string + :param mail_host: The email host name of the domain to remove. + :type mail_host: string :return: The domain object. :rtype: `IDomain` :raises KeyError: if the named domain does not exist. """ - def get(email_host, default=None): + def get(mail_host, default=None): """Return the named domain. - :param email_host: The email host name of the domain to remove. - :type email_host: string + :param mail_host: The email host name of the domain to remove. + :type mail_host: string :param default: What to return if the named domain does not exist. :type default: object :return: The domain object or None if the named domain does not exist. @@ -136,11 +136,11 @@ class IDomainManager(Interface): :return: iterator over `IDomain`. """ - def __contains__(email_host): + def __contains__(mail_host): """Is this a known domain? - :param email_host: An email host name. - :type email_host: string + :param mail_host: An email host name. + :type mail_host: string :return: True if this domain is known. :rtype: bool """ diff --git a/src/mailman/model/docs/domains.txt b/src/mailman/model/docs/domains.rst index 00824d65c..924ab7301 100644 --- a/src/mailman/model/docs/domains.txt +++ b/src/mailman/model/docs/domains.rst @@ -18,7 +18,7 @@ Domains are how Mailman interacts with email host names and web host names. ... if len(manager) == 0: ... print 'no domains' ... return - ... for domain in sorted(manager, key=attrgetter('email_host')): + ... for domain in sorted(manager, key=attrgetter('mail_host')): ... print domain >>> show_domains() @@ -97,8 +97,8 @@ property. In the global domain manager, domains are indexed by their email host name. :: - >>> for domain in sorted(manager, key=attrgetter('email_host')): - ... print domain.email_host + >>> for domain in sorted(manager, key=attrgetter('mail_host')): + ... print domain.mail_host example.com example.net diff --git a/src/mailman/model/docs/requests.txt b/src/mailman/model/docs/requests.rst index 812d25a43..e01544490 100644 --- a/src/mailman/model/docs/requests.txt +++ b/src/mailman/model/docs/requests.rst @@ -312,7 +312,7 @@ indicates that the message has been approved. >>> id_3 = moderator.hold_message(mlist, msg, msgdata, 'Needs approval') >>> moderator.handle_message(mlist, id_3, Action.accept) - >>> inq = config.switchboards['in'] + >>> inq = config.switchboards['pipeline'] >>> qmsg, qdata = dequeue(inq) >>> print qmsg.as_string() From: aperson@example.org diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py index 95710171f..2c0d35082 100644 --- a/src/mailman/model/domain.py +++ b/src/mailman/model/domain.py @@ -44,37 +44,37 @@ class Domain(Model): id = Int(primary=True) - email_host = Unicode() + mail_host = Unicode() base_url = Unicode() description = Unicode() contact_address = Unicode() - def __init__(self, email_host, + def __init__(self, mail_host, description=None, base_url=None, contact_address=None): """Create and register a domain. - :param email_host: The host name for the email interface. - :type email_host: string + :param mail_host: The host name for the email interface. + :type mail_host: string :param description: An optional description of the domain. :type description: string :param base_url: The optional base url for the domain, including scheme. If not given, it will be constructed from the - `email_host` using the http protocol. + `mail_host` using the http protocol. :type base_url: string :param contact_address: The email address to contact a human for this - domain. If not given, postmaster@`email_host` will be used. + domain. If not given, postmaster@`mail_host` will be used. :type contact_address: string """ - self.email_host = email_host + self.mail_host = mail_host self.base_url = (base_url if base_url is not None - else 'http://' + email_host) + else 'http://' + mail_host) self.description = description self.contact_address = (contact_address if contact_address is not None - else 'postmaster@' + email_host) + else 'postmaster@' + mail_host) @property def url_host(self): @@ -101,10 +101,10 @@ class Domain(Model): def __repr__(self): """repr(a_domain)""" if self.description is None: - return ('<Domain {0.email_host}, base_url: {0.base_url}, ' + return ('<Domain {0.mail_host}, base_url: {0.base_url}, ' 'contact_address: {0.contact_address}>').format(self) else: - return ('<Domain {0.email_host}, {0.description}, ' + return ('<Domain {0.mail_host}, {0.description}, ' 'base_url: {0.base_url}, ' 'contact_address: {0.contact_address}>').format(self) @@ -115,40 +115,40 @@ class DomainManager: implements(IDomainManager) - def add(self, email_host, + def add(self, mail_host, description=None, base_url=None, contact_address=None): """See `IDomainManager`.""" - # Be sure the email_host is not already registered. This is probably + # Be sure the mail_host is not already registered. This is probably # a constraint that should (also) be maintained in the database. - if self.get(email_host) is not None: + if self.get(mail_host) is not None: raise BadDomainSpecificationError( - 'Duplicate email host: %s' % email_host) - domain = Domain(email_host, description, base_url, contact_address) + 'Duplicate email host: %s' % mail_host) + domain = Domain(mail_host, description, base_url, contact_address) config.db.store.add(domain) return domain - def remove(self, email_host): - domain = self[email_host] + def remove(self, mail_host): + domain = self[mail_host] config.db.store.remove(domain) return domain - def get(self, email_host, default=None): + def get(self, mail_host, default=None): """See `IDomainManager`.""" - domains = config.db.store.find(Domain, email_host=email_host) + domains = config.db.store.find(Domain, mail_host=mail_host) if domains.count() < 1: return default assert domains.count() == 1, ( - 'Too many matching domains: %s' % email_host) + 'Too many matching domains: %s' % mail_host) return domains.one() - def __getitem__(self, email_host): + def __getitem__(self, mail_host): """See `IDomainManager`.""" missing = object() - domain = self.get(email_host, missing) + domain = self.get(mail_host, missing) if domain is missing: - raise KeyError(email_host) + raise KeyError(mail_host) return domain def __len__(self): @@ -159,6 +159,6 @@ class DomainManager: for domain in config.db.store.find(Domain): yield domain - def __contains__(self, email_host): + def __contains__(self, mail_host): """See `IDomainManager`.""" - return config.db.store.find(Domain, email_host=email_host).count() > 0 + return config.db.store.find(Domain, mail_host=mail_host).count() > 0 diff --git a/src/mailman/rest/docs/domains.txt b/src/mailman/rest/docs/domains.rst index 293d54c98..9a2708ac9 100644 --- a/src/mailman/rest/docs/domains.txt +++ b/src/mailman/rest/docs/domains.rst @@ -38,8 +38,8 @@ Once a domain is added, it is accessible through the API. base_url: http://lists.example.com contact_address: postmaster@example.com description: An example domain - email_host: example.com http_etag: "..." + mail_host: example.com self_link: http://localhost:9001/3.0/domains/example.com url_host: lists.example.com http_etag: "..." @@ -70,24 +70,24 @@ At the top level, all domains are returned as separate entries. base_url: http://lists.example.com contact_address: postmaster@example.com description: An example domain - email_host: example.com http_etag: "..." + mail_host: example.com self_link: http://localhost:9001/3.0/domains/example.com url_host: lists.example.com entry 1: base_url: http://mail.example.org contact_address: listmaster@example.org description: None - email_host: example.org http_etag: "..." + mail_host: example.org self_link: http://localhost:9001/3.0/domains/example.org url_host: mail.example.org entry 2: base_url: http://example.net contact_address: porkmaster@example.net description: Porkmasters - email_host: lists.example.net http_etag: "..." + mail_host: lists.example.net self_link: http://localhost:9001/3.0/domains/lists.example.net url_host: example.net http_etag: "..." @@ -105,8 +105,8 @@ The information for a single domain is available by following one of the base_url: http://example.net contact_address: porkmaster@example.net description: Porkmasters - email_host: lists.example.net http_etag: "..." + mail_host: lists.example.net self_link: http://localhost:9001/3.0/domains/lists.example.net url_host: example.net @@ -167,7 +167,7 @@ Creating new domains New domains can be created by posting to the ``domains`` url. >>> dump_json('http://localhost:9001/3.0/domains', { - ... 'email_host': 'lists.example.com', + ... 'mail_host': 'lists.example.com', ... }) content-length: 0 date: ... @@ -180,8 +180,8 @@ Now the web service knows about our new domain. base_url: http://lists.example.com contact_address: postmaster@lists.example.com description: None - email_host: lists.example.com http_etag: "..." + mail_host: lists.example.com self_link: http://localhost:9001/3.0/domains/lists.example.com url_host: lists.example.com @@ -201,7 +201,7 @@ address. :: >>> dump_json('http://localhost:9001/3.0/domains', { - ... 'email_host': 'my.example.com', + ... 'mail_host': 'my.example.com', ... 'description': 'My new domain', ... 'base_url': 'http://allmy.example.com', ... 'contact_address': 'helpme@example.com' @@ -215,8 +215,8 @@ address. base_url: http://allmy.example.com contact_address: helpme@example.com description: My new domain - email_host: my.example.com http_etag: "..." + mail_host: my.example.com self_link: http://localhost:9001/3.0/domains/my.example.com url_host: allmy.example.com diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index 61cc28ca0..ca477888c 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -46,8 +46,8 @@ class _DomainBase(resource.Resource, CollectionMixin): base_url=domain.base_url, contact_address=domain.contact_address, description=domain.description, - email_host=domain.email_host, - self_link=path_to('domains/{0}'.format(domain.email_host)), + mail_host=domain.mail_host, + self_link=path_to('domains/{0}'.format(domain.mail_host)), url_host=domain.url_host, ) @@ -100,7 +100,7 @@ class AllDomains(_DomainBase): """Create a new domain.""" domain_manager = getUtility(IDomainManager) try: - validator = Validator(email_host=unicode, + validator = Validator(mail_host=unicode, description=unicode, base_url=unicode, contact_address=unicode, @@ -111,7 +111,7 @@ class AllDomains(_DomainBase): return http.bad_request([], b'Domain exists') except ValueError as error: return http.bad_request([], str(error)) - location = path_to('domains/{0}'.format(domain.email_host)) + location = path_to('domains/{0}'.format(domain.mail_host)) # Include no extra headers or body. return http.created(location, [], None) diff --git a/src/mailman/runners/outgoing.py b/src/mailman/runners/outgoing.py index 65d8928a6..e771d8be3 100644 --- a/src/mailman/runners/outgoing.py +++ b/src/mailman/runners/outgoing.py @@ -41,6 +41,7 @@ DEAL_WITH_PERMFAILURES_EVERY = 10 log = logging.getLogger('mailman.error') smtp_log = logging.getLogger('mailman.smtp') +debug_log = logging.getLogger('mailman.debug') @@ -86,6 +87,8 @@ class OutgoingRunner(Runner): # VERP every 'interval' number of times. msgdata['verp'] = (mlist.post_id % interval == 0) try: + debug_log.debug('[outgoing] {0}: {1}'.format( + self._func, msg.get('message-id', 'n/a'))) self._func(mlist, msg, msgdata) self._logged = False except socket.error: |
