~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/mail/handler.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-06-25 08:55:37 UTC
  • mfrom: (13287.1.8 bug-800652)
  • Revision ID: launchpad@pqm.canonical.com-20110625085537-moikyoo2pe98zs7r
[r=jcsackett, julian-edwards][bug=800634,
        800652] Enable and display overrides on sync package uploads.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
3
 
4
4
"""Handle incoming Bugs email."""
8
8
    "MaloneHandler",
9
9
    ]
10
10
 
11
 
from operator import attrgetter
12
 
import os
13
 
 
14
11
from lazr.lifecycle.event import ObjectCreatedEvent
15
12
from lazr.lifecycle.interfaces import IObjectCreatedEvent
16
 
import transaction
17
13
from zope.component import getUtility
18
14
from zope.event import notify
19
15
from zope.interface import implements
20
16
 
21
 
from lp.bugs.interfaces.bug import (
22
 
    CreateBugParams,
23
 
    CreatedBugWithNoBugTasksError,
24
 
    )
25
 
from lp.bugs.interfaces.bugattachment import (
26
 
    BugAttachmentType,
27
 
    IBugAttachmentSet,
28
 
    )
29
 
from lp.bugs.interfaces.bugmessage import IBugMessageSet
30
 
from lp.bugs.mail.commands import BugEmailCommands
31
 
from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
32
 
from lp.services.mail.helpers import (
 
17
from canonical.database.sqlbase import rollback
 
18
from canonical.launchpad.helpers import get_email_template
 
19
from canonical.launchpad.interfaces.mail import (
 
20
    EmailProcessingError,
 
21
    IBugEditEmailCommand,
 
22
    IBugEmailCommand,
 
23
    IBugTaskEditEmailCommand,
 
24
    IBugTaskEmailCommand,
 
25
    IMailHandler,
 
26
    )
 
27
from lp.services.messages.interfaces.message import IMessageSet
 
28
from canonical.launchpad.mail.commands import (
 
29
    BugEmailCommands,
 
30
    get_error_message,
 
31
    )
 
32
from canonical.launchpad.mail.helpers import (
33
33
    ensure_not_weakly_authenticated,
34
 
    get_email_template,
35
 
    get_error_message,
36
34
    get_main_body,
37
35
    guess_bugtask,
38
36
    IncomingEmailError,
39
37
    parse_commands,
40
38
    reformat_wiki_text,
41
39
    )
42
 
from lp.services.mail.interfaces import (
43
 
    EmailProcessingError,
44
 
    IBugEditEmailCommand,
45
 
    IBugEmailCommand,
46
 
    IBugTaskEditEmailCommand,
47
 
    IBugTaskEmailCommand,
48
 
    IMailHandler,
49
 
    )
50
 
from lp.services.mail.mailwrapper import MailWrapper
51
 
from lp.services.mail.notification import send_process_error_notification
 
40
from canonical.launchpad.mailnotification import (
 
41
    MailWrapper,
 
42
    send_process_error_notification,
 
43
    )
 
44
from canonical.launchpad.webapp.interfaces import ILaunchBag
 
45
from lp.bugs.interfaces.bug import CreatedBugWithNoBugTasksError
 
46
from lp.bugs.interfaces.bugattachment import (
 
47
    BugAttachmentType,
 
48
    IBugAttachmentSet,
 
49
    )
 
50
from lp.bugs.interfaces.bugmessage import IBugMessageSet
52
51
from lp.services.mail.sendmail import simple_sendmail
53
 
from lp.services.messages.interfaces.message import IMessageSet
54
 
from lp.services.webapp.interfaces import ILaunchBag
55
 
 
56
 
 
57
 
error_templates = os.path.join(os.path.dirname(__file__), 'errortemplates')
58
 
 
59
 
 
60
 
class BugTaskCommandGroup:
61
 
 
62
 
    def __init__(self, command=None):
63
 
        self._commands = []
64
 
        if command is not None:
65
 
            self._commands.append(command)
66
 
 
67
 
    def __nonzero__(self):
68
 
        return len(self._commands) > 0
69
 
 
70
 
    def __str__(self):
71
 
        text_commands = [str(cmd) for cmd in self.commands]
72
 
        return '\n'.join(text_commands).strip()
73
 
 
74
 
    @property
75
 
    def commands(self):
76
 
        "Return the `EmailCommand`s ordered by their rank."
77
 
        return sorted(self._commands, key=attrgetter('RANK'))
78
 
 
79
 
    def add(self, command):
80
 
        "Add an `EmailCommand` to the commands."
81
 
        self._commands.append(command)
82
 
 
83
 
 
84
 
class BugCommandGroup(BugTaskCommandGroup):
85
 
 
86
 
    def __init__(self, command=None):
87
 
        super(BugCommandGroup, self).__init__(command=command)
88
 
        self._groups = []
89
 
 
90
 
    def __nonzero__(self):
91
 
        if len(self._groups) > 0:
92
 
            return True
93
 
        else:
94
 
            return super(BugCommandGroup, self).__nonzero__()
95
 
 
96
 
    def __str__(self):
97
 
        text_commands = [super(BugCommandGroup, self).__str__()]
98
 
        for group in self.groups:
99
 
            text_commands += [str(group)]
100
 
        return '\n'.join(text_commands).strip()
101
 
 
102
 
    @property
103
 
    def groups(self):
104
 
        "Return the `BugTaskCommandGroup`s."
105
 
        is_new_bug = (
106
 
            len(self.commands) > 0
107
 
            and self.commands[0].RANK == 0
108
 
            and self.commands[0].string_args == ['new'])
109
 
        has_split_affects = (
110
 
            len(self._groups) == 2
111
 
            and self._groups[0].commands[0].RANK != 0
112
 
            and self._groups[1].commands[0].RANK == 0)
113
 
        if is_new_bug and has_split_affects:
114
 
            # The affects line was in the wrong position and this
115
 
            # exact case can be fixed.
116
 
            self._groups[0]._commands += self._groups[1]._commands
117
 
            del self._groups[1]
118
 
        return list(self._groups)
119
 
 
120
 
    def add(self, command_or_group):
121
 
        """Add an `EmailCommand` or `BugTaskCommandGroup` to the commands.
122
 
 
123
 
        Empty BugTaskCommandGroup are ignored.
124
 
        """
125
 
        if isinstance(command_or_group, BugTaskCommandGroup):
126
 
            if command_or_group:
127
 
                self._groups.append(command_or_group)
128
 
        else:
129
 
            super(BugCommandGroup, self).add(command_or_group)
130
 
 
131
 
 
132
 
class BugCommandGroups(BugCommandGroup):
133
 
 
134
 
    def __init__(self, commands):
135
 
        super(BugCommandGroups, self).__init__(command=None)
136
 
        self._groups = []
137
 
        this_bug = BugCommandGroup()
138
 
        this_bugtask = BugTaskCommandGroup()
139
 
        for command in commands:
140
 
            if IBugEmailCommand.providedBy(command) and command.RANK == 0:
141
 
                # Multiple bugs are being edited.
142
 
                this_bug.add(this_bugtask)
143
 
                self.add(this_bug)
144
 
                this_bug = BugCommandGroup(command)
145
 
                this_bugtask = BugTaskCommandGroup()
146
 
            elif IBugEditEmailCommand.providedBy(command):
147
 
                this_bug.add(command)
148
 
            elif (IBugTaskEmailCommand.providedBy(command)
149
 
                  and command.RANK == 0):
150
 
                # Multiple or explicit bugtasks are being edited.
151
 
                this_bug.add(this_bugtask)
152
 
                this_bugtask = BugTaskCommandGroup(command)
153
 
            elif IBugTaskEditEmailCommand.providedBy(command):
154
 
                this_bugtask.add(command)
155
 
        this_bug.add(this_bugtask)
156
 
        self.add(this_bug)
157
 
 
158
 
    def __iter__(self):
159
 
        for bug_group in self.groups:
160
 
            for command in bug_group.commands:
161
 
                yield command
162
 
            for bugtask_group in bug_group.groups:
163
 
                for command in bugtask_group.commands:
164
 
                    yield command
165
 
 
166
 
    def add(self, command_or_group):
167
 
        """Add a `BugCommandGroup` to the groups of commands.
168
 
 
169
 
        Empty BugCommandGroups are ignored.
170
 
        """
171
 
        if isinstance(command_or_group, BugCommandGroup):
172
 
            if command_or_group:
173
 
                self._groups.append(command_or_group)
174
52
 
175
53
 
176
54
class MaloneHandler:
210
88
        commands = self.getCommands(signed_msg)
211
89
        to_user, to_host = to_addr.split('@')
212
90
        add_comment_to_bug = False
213
 
        from_user = getUtility(ILaunchBag).user
214
 
        if to_user.lower() == 'help' or from_user is None:
215
 
            if from_user is not None and from_user.preferredemail is not None:
216
 
                to_address = str(from_user.preferredemail.email)
217
 
            else:
218
 
                to_address = signed_msg['From']
219
 
                address = getUtility(IEmailAddressSet).getByEmail(to_address)
220
 
                if address is None:
221
 
                    to_address = None
222
 
            if to_address is not None:
223
 
                self.sendHelpEmail(to_address)
224
 
            return True, False, None
225
91
        # If there are any commands, we must have strong authentication.
226
92
        # We send a different failure message for attempts to create a new
227
93
        # bug.
228
 
        elif to_user.lower() == 'new':
 
94
        if to_user.lower() == 'new':
229
95
            ensure_not_weakly_authenticated(signed_msg, CONTEXT,
230
 
                'unauthenticated-bug-creation.txt',
231
 
                error_templates=error_templates)
 
96
                'unauthenticated-bug-creation.txt')
232
97
        elif len(commands) > 0:
233
98
            ensure_not_weakly_authenticated(signed_msg, CONTEXT)
234
99
        if to_user.lower() == 'new':
241
106
            # the bug.
242
107
            add_comment_to_bug = True
243
108
            commands.insert(0, BugEmailCommands.get('bug', [to_user]))
 
109
        elif to_user.lower() == 'help':
 
110
            from_user = getUtility(ILaunchBag).user
 
111
            if from_user is not None:
 
112
                preferredemail = from_user.preferredemail
 
113
                if preferredemail is not None:
 
114
                    to_address = str(preferredemail.email)
 
115
                    self.sendHelpEmail(to_address)
 
116
            return True, False, None
244
117
        elif to_user.lower() != 'edit':
245
118
            # Indicate that we didn't handle the mail.
246
119
            return False, False, None
247
 
        bug_commands = list(BugCommandGroups(commands))
248
 
        return None, add_comment_to_bug, bug_commands
 
120
        return None, add_comment_to_bug, commands
249
121
 
250
122
    def process(self, signed_msg, to_addr, filealias=None, log=None):
251
123
        """See IMailHandler."""
267
139
                command = commands.pop(0)
268
140
                try:
269
141
                    if IBugEmailCommand.providedBy(command):
270
 
                        # Finish outstanding work from the previous bug.
271
 
                        self.notify_bug_event(bug_event)
272
 
                        self.notify_bugtask_event(bugtask_event, bug_event)
 
142
                        if bug_event is not None:
 
143
                            try:
 
144
                                notify(bug_event)
 
145
                            except CreatedBugWithNoBugTasksError:
 
146
                                rollback()
 
147
                                raise IncomingEmailError(
 
148
                                    get_error_message(
 
149
                                        'no-affects-target-on-submit.txt'))
 
150
                        if (bugtask_event is not None and
 
151
                            not IObjectCreatedEvent.providedBy(bug_event)):
 
152
                            notify(bugtask_event)
273
153
                        bugtask = None
274
154
                        bugtask_event = None
275
 
                        # Get or start building a new bug.
 
155
 
276
156
                        bug, bug_event = command.execute(
277
157
                            signed_msg, filealias)
278
158
                        if add_comment_to_bug:
279
 
                            message = self.appendBugComment(
280
 
                                bug, signed_msg, filealias)
 
159
                            messageset = getUtility(IMessageSet)
 
160
                            message = messageset.fromEmail(
 
161
                                signed_msg.as_string(),
 
162
                                owner=getUtility(ILaunchBag).user,
 
163
                                filealias=filealias,
 
164
                                parsed_message=signed_msg,
 
165
                                fallback_parent=bug.initial_message)
 
166
 
 
167
                            # If the new message's parent is linked to
 
168
                            # a bug watch we also link this message to
 
169
                            # that bug watch.
 
170
                            bug_message_set = getUtility(IBugMessageSet)
 
171
                            parent_bug_message = (
 
172
                                bug_message_set.getByBugAndMessage(
 
173
                                    bug, message.parent))
 
174
 
 
175
                            if (parent_bug_message is not None and
 
176
                                parent_bug_message.bugwatch):
 
177
                                bug_watch = parent_bug_message.bugwatch
 
178
                            else:
 
179
                                bug_watch = None
 
180
 
 
181
                            bugmessage = bug.linkMessage(
 
182
                                message, bug_watch)
 
183
 
 
184
                            notify(ObjectCreatedEvent(bugmessage))
281
185
                            add_comment_to_bug = False
282
 
                            self.processAttachments(bug, message, signed_msg)
 
186
                        else:
 
187
                            message = bug.initial_message
 
188
                        self.processAttachments(bug, message, signed_msg)
283
189
                    elif IBugTaskEmailCommand.providedBy(command):
284
 
                        self.notify_bugtask_event(bugtask_event, bug_event)
285
 
                        bugtask, bugtask_event, bug_event = command.execute(
286
 
                            bug, bug_event)
287
 
                        if isinstance(bug, CreateBugParams):
288
 
                            bug = bugtask.bug
289
 
                            message = bug.initial_message
290
 
                            self.processAttachments(bug, message, signed_msg)
 
190
                        if bugtask_event is not None:
 
191
                            if not IObjectCreatedEvent.providedBy(bug_event):
 
192
                                notify(bugtask_event)
 
193
                            bugtask_event = None
 
194
                        bugtask, bugtask_event = command.execute(bug)
291
195
                    elif IBugEditEmailCommand.providedBy(command):
292
196
                        bug, bug_event = command.execute(bug, bug_event)
293
197
                    elif IBugTaskEditEmailCommand.providedBy(command):
294
198
                        if bugtask is None:
295
 
                            if isinstance(bug, CreateBugParams):
296
 
                                self.handleNoAffectsTarget()
 
199
                            if len(bug.bugtasks) == 0:
 
200
                                rollback()
 
201
                                raise IncomingEmailError(
 
202
                                    get_error_message(
 
203
                                        'no-affects-target-on-submit.txt'))
297
204
                            bugtask = guess_bugtask(
298
205
                                bug, getUtility(ILaunchBag).user)
299
206
                            if bugtask is None:
300
 
                                self.handleNoDefaultAffectsTarget(bug)
 
207
                                raise IncomingEmailError(get_error_message(
 
208
                                    'no-default-affects.txt',
 
209
                                    bug_id=bug.id,
 
210
                                    nr_of_bugtasks=len(bug.bugtasks)))
301
211
                        bugtask, bugtask_event = command.execute(
302
212
                            bugtask, bugtask_event)
303
213
 
305
215
                    processing_errors.append((error, command))
306
216
                    if error.stop_processing:
307
217
                        commands = []
308
 
                        transaction.abort()
 
218
                        rollback()
309
219
                    else:
310
220
                        continue
311
221
 
314
224
                    '\n'.join(str(error) for error, command
315
225
                              in processing_errors),
316
226
                    [command for error, command in processing_errors])
317
 
            if isinstance(bug, CreateBugParams):
318
 
                # A new bug without any commands was sent.
319
 
                self.handleNoAffectsTarget()
320
 
            self.notify_bug_event(bug_event)
321
 
            self.notify_bugtask_event(bugtask_event, bug_event)
 
227
 
 
228
            if bug_event is not None:
 
229
                try:
 
230
                    notify(bug_event)
 
231
                except CreatedBugWithNoBugTasksError:
 
232
                    rollback()
 
233
                    raise IncomingEmailError(
 
234
                        get_error_message('no-affects-target-on-submit.txt'))
 
235
            if bugtask_event is not None:
 
236
                if not IObjectCreatedEvent.providedBy(bug_event):
 
237
                    notify(bugtask_event)
322
238
 
323
239
        except IncomingEmailError, error:
324
240
            send_process_error_notification(
331
247
    def sendHelpEmail(self, to_address):
332
248
        """Send usage help to `to_address`."""
333
249
        # Get the help text (formatted as MoinMoin markup)
334
 
        help_text = get_email_template('help.txt', app='bugs')
 
250
        help_text = get_email_template('help.txt')
335
251
        help_text = reformat_wiki_text(help_text)
336
252
        # Wrap text
337
253
        mailwrapper = MailWrapper(width=72)
358
274
    # seem to store no more than an RTF representation of an email.
359
275
 
360
276
    irrelevant_content_types = set((
361
 
        'application/applefile',  # the resource fork of a MacOS file
 
277
        'application/applefile', # the resource fork of a MacOS file
362
278
        'application/pgp-signature',
363
279
        'application/pkcs7-signature',
364
280
        'application/x-pkcs7-signature',
395
311
            getUtility(IBugAttachmentSet).create(
396
312
                bug=bug, filealias=blob, attach_type=attach_type,
397
313
                title=blob.filename, message=message, send_notifications=True)
398
 
 
399
 
    def appendBugComment(self, bug, signed_msg, filealias=None):
400
 
        """Append the message text to the bug comments."""
401
 
        messageset = getUtility(IMessageSet)
402
 
        message = messageset.fromEmail(
403
 
            signed_msg.as_string(),
404
 
            owner=getUtility(ILaunchBag).user,
405
 
            filealias=filealias,
406
 
            parsed_message=signed_msg,
407
 
            fallback_parent=bug.initial_message)
408
 
        # If the new message's parent is linked to
409
 
        # a bug watch we also link this message to
410
 
        # that bug watch.
411
 
        bug_message_set = getUtility(IBugMessageSet)
412
 
        parent_bug_message = (
413
 
            bug_message_set.getByBugAndMessage(bug, message.parent))
414
 
        if (parent_bug_message is not None and
415
 
            parent_bug_message.bugwatch):
416
 
            bug_watch = parent_bug_message.bugwatch
417
 
        else:
418
 
            bug_watch = None
419
 
        bugmessage = bug.linkMessage(
420
 
            message, bug_watch)
421
 
        notify(ObjectCreatedEvent(bugmessage))
422
 
        return message
423
 
 
424
 
    def notify_bug_event(self, bug_event):
425
 
        if bug_event is  None:
426
 
            return
427
 
        try:
428
 
            notify(bug_event)
429
 
        except CreatedBugWithNoBugTasksError:
430
 
            self.handleNoAffectsTarget()
431
 
 
432
 
    def notify_bugtask_event(self, bugtask_event, bug_event):
433
 
            if bugtask_event is None:
434
 
                return
435
 
            if not IObjectCreatedEvent.providedBy(bug_event):
436
 
                notify(bugtask_event)
437
 
 
438
 
    def handleNoAffectsTarget(self):
439
 
        transaction.abort()
440
 
        raise IncomingEmailError(
441
 
            get_error_message(
442
 
                'no-affects-target-on-submit.txt',
443
 
                error_templates=error_templates))
444
 
 
445
 
    def handleNoDefaultAffectsTarget(self, bug):
446
 
        raise IncomingEmailError(get_error_message(
447
 
            'no-default-affects.txt',
448
 
            error_templates=error_templates,
449
 
            bug_id=bug.id,
450
 
            nr_of_bugtasks=len(bug.bugtasks)))