launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03763
[Merge] lp:~abentley/launchpad/duplicate-msgid into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/duplicate-msgid into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #595166 in Launchpad itself: "IntegrityError raised filing a bug using the email interface"
https://bugs.launchpad.net/launchpad/+bug/595166
For more details, see:
https://code.launchpad.net/~abentley/launchpad/duplicate-msgid/+merge/62697
= Summary =
Fix bug #595166: IntegrityError raised filing a bug using the email interface
== Proposed fix ==
Skip all incoming messages whose message-ids duplicate messages already in the database.
== Pre-implementation notes ==
Discussed with sinzui
== Implementation details ==
The IntegrityError comes from trying to link the same message to two different bugs. This can happen if a message with identical contents is received twice. While there may be times we want to permit duplicate message-ids, but not when processing incoming commands.
== Tests ==
bin/test -t duplicate_message_id test_incoming
== Demo and Q/A ==
Send an email to launchpad to create a bug. Then re-send that message with the same message-id. The second message should be ignored.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/services/mail/incoming.py
lib/lp/services/mail/tests/test_incoming.py
lib/lp/services/mail/tests/__init__.py
--
https://code.launchpad.net/~abentley/launchpad/duplicate-msgid/+merge/62697
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/duplicate-msgid into lp:launchpad.
=== modified file 'lib/lp/services/mail/incoming.py'
--- lib/lp/services/mail/incoming.py 2011-05-09 20:04:32 +0000
+++ lib/lp/services/mail/incoming.py 2011-05-27 15:33:28 +0000
@@ -50,11 +50,13 @@
)
from canonical.launchpad.webapp.interfaces import IPlacelessAuthUtility
from canonical.librarian.interfaces import UploadFailed
+from lp.app.errors import NotFoundError
from lp.registry.interfaces.person import IPerson
from lp.services.features import getFeatureFlag
from lp.services.mail.handlers import mail_handlers
from lp.services.mail.sendmail import do_paranoid_envelope_to_validation
from lp.services.mail.signedmessage import signed_message_from_string
+from lp.services.messages.interfaces.message import IMessageSet
# Match '\n' and '\r' line endings. That is, all '\r' that are not
# followed by a '\n', and all '\n' that are not preceded by a '\r'.
@@ -424,6 +426,16 @@
log.debug('processing mail from %r message-id %r' %
(mail['from'], mail['message-id']))
+ msg_set = getUtility(IMessageSet)
+ try:
+ msg_set.get(mail['message-id'])
+ except NotFoundError:
+ pass
+ else:
+ log.warning(
+ 'Message with id %s already stored. Skipping.',
+ mail['message-id'])
+ return None
# If the Return-Path header is '<>', it probably means
# that it's a bounce from a message we sent.
=== modified file 'lib/lp/services/mail/tests/__init__.py'
--- lib/lp/services/mail/tests/__init__.py 2009-05-11 04:14:52 +0000
+++ lib/lp/services/mail/tests/__init__.py 2011-05-27 15:33:28 +0000
@@ -0,0 +1,32 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for mail services."""
+
+__metaclass__ = type
+__all__ = ['ListHandler']
+
+
+from contextlib import contextmanager
+
+from lp.services.mail.handlers import mail_handlers
+
+
+class ListHandler:
+ """A mail handler that simply adds the message to a list."""
+ def __init__(self):
+ self.processed = []
+
+ def process(self, signed_msg, to_addr, filealias=None, log=None):
+ self.processed.append(signed_msg)
+ return True
+
+
+@contextmanager
+def active_handler(domain, handler):
+ """In the context, the specified handler is registered for the domain."""
+ mail_handlers.add(domain, handler)
+ try:
+ yield handler
+ finally:
+ del mail_handlers._handlers[domain]
=== modified file 'lib/lp/services/mail/tests/test_incoming.py'
--- lib/lp/services/mail/tests/test_incoming.py 2011-05-07 15:35:16 +0000
+++ lib/lp/services/mail/tests/test_incoming.py 2011-05-27 15:33:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
from doctest import DocTestSuite
@@ -6,7 +6,11 @@
import os
import unittest
-from testtools.matchers import Equals, Is
+from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesRegex,
+ )
import transaction
from zope.security.management import setSecurityPolicy
@@ -24,11 +28,17 @@
authenticateEmail,
extract_addresses,
handleMail,
+ handle_one_mail,
MailErrorUtility,
ORIGINAL_TO_HEADER,
)
from lp.services.mail.sendmail import MailController
from lp.services.mail.stub import TestMailer
+from lp.services.messages.model.message import MessageSet
+from lp.services.mail.tests import (
+ active_handler,
+ ListHandler,
+ )
from lp.testing import TestCaseWithFactory
from lp.testing.factory import GPGSigningContext
from lp.testing.mail_helpers import pop_notifications
@@ -122,6 +132,25 @@
mail = self.factory.makeSignedMessage(email_address=email)
self.assertThat(authenticateEmail(mail), Is(None))
+ def test_handle_one_mail_duplicate_message_id(self):
+ # An incoming mail with the message-id of an already-processed mail is
+ # skipped.
+ orig_message = self.factory.makeMessage()
+ handler_calls = []
+ self.assertEqual([], handler_calls)
+ duplicate_mail = self.factory.makeSignedMessage(
+ msgid=orig_message.rfc822msgid,
+ to_address='new@xxxxxxxxxxxxxxxxxx')
+ log = BufferLogger()
+ with active_handler('random.example.com', ListHandler()) as handler:
+ handle_one_mail(log, duplicate_mail, None, None, None)
+ self.assertEqual([], handler.processed)
+ messages = MessageSet().get(orig_message.rfc822msgid)
+ self.assertEqual(1, len(messages))
+ matches = MatchesRegex(
+ '(.|\n)*WARNING Message with id .* already stored. Skipping.')
+ self.assertThat(log.getLogBuffer(), matches)
+
class TestExtractAddresses(TestCaseWithFactory):