~launchpad-pqm/launchpad/devel

13174.1.1 by Martin Pool
Unify implementations of save-mail-to-librarian; both use uuids for file names
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
3
4
__metaclass__ = type
5
13174.1.1 by Martin Pool
Unify implementations of save-mail-to-librarian; both use uuids for file names
6
from cStringIO import StringIO as cStringIO
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
7
import os.path
8
import re
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
9
import time
13174.1.1 by Martin Pool
Unify implementations of save-mail-to-librarian; both use uuids for file names
10
from uuid import uuid1
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
11
12
from zope.component import getUtility
13
14
from canonical.launchpad.webapp import canonical_url
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
15
from canonical.launchpad.webapp.interaction import get_current_principal
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
16
from canonical.launchpad.webapp.interfaces import ILaunchBag
7675.161.1 by Curtis Hovey
Ported the original registry vocab branch to db-devel. This means that the classes and tests for ValidPersonOrTeam, Milestone, and DistributionOrProduct were reconciled.
17
from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
18
from lp.services.librarian.interfaces import ILibraryFileAliasSet
13668.1.22 by Curtis Hovey
Sorted imports.
19
from lp.services.mail.interfaces import (
20
    EmailProcessingError,
21
    IWeaklyAuthenticatedPrincipal,
22
    )
7675.161.1 by Curtis Hovey
Ported the original registry vocab branch to db-devel. This means that the classes and tests for ValidPersonOrTeam, Milestone, and DistributionOrProduct were reconciled.
23
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
24
25
class IncomingEmailError(Exception):
26
    """Indicates that something went wrong processing the mail."""
27
28
    def __init__(self, message, failing_command=None):
29
        Exception.__init__(self, message)
30
        self.message = message
31
        self.failing_command = failing_command
32
33
34
def get_main_body(signed_msg):
35
    """Returns the first text part of the email."""
7675.6.8 by Tim Penhey
Yay, tests pass.
36
    msg = getattr(signed_msg, 'signedMessage', None)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
37
    if msg is None:
38
        # The email wasn't signed.
39
        msg = signed_msg
40
    if msg.is_multipart():
7675.6.8 by Tim Penhey
Yay, tests pass.
41
        for part in msg.walk():
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
42
            if part.get_content_type() == 'text/plain':
43
                return part.get_payload(decode=True)
44
    else:
45
        return msg.get_payload(decode=True)
46
47
48
def guess_bugtask(bug, person):
49
    """Guess which bug task the person intended to edit.
50
51
    Return None if no bug task could be guessed.
52
    """
53
    if len(bug.bugtasks) == 1:
54
        return bug.bugtasks[0]
55
    else:
56
        for bugtask in bug.bugtasks:
13479.2.6 by William Grant
No more I*BugTask in c.l.mail.helpers.
57
            if bugtask.product:
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
58
                # Is the person an upstream maintainer?
59
                if person.inTeam(bugtask.product.owner):
60
                    return bugtask
13479.2.6 by William Grant
No more I*BugTask in c.l.mail.helpers.
61
            elif bugtask.distribution:
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
62
                # Is the person a member of the distribution?
63
                if person.inTeam(bugtask.distribution.members):
64
                    return bugtask
65
                else:
66
                    # Is the person one of the package subscribers?
67
                    bug_sub = bugtask.target.getSubscription(person)
68
                    if bug_sub is not None:
7675.1028.1 by Gary Poster
non-browser changes are sketched. browser changes and actually trying to get it to work remain. :-P
69
                        return bugtask
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
70
    return None
71
72
73
def reformat_wiki_text(text):
74
    """Transform moin formatted raw text to readable text."""
75
76
    # XXX Tom Berger 2008-02-20 bug=193646:
77
    # This implementation is neither correct nor complete.
78
79
    # Strip macros (anchors, TOC, etc'...)
80
    re_macro = re.compile('\[\[.*?\]\]')
81
    text = re_macro.sub('', text)
82
83
    # sterilize links
84
    re_link = re.compile('\[(.*?)\]')
85
    text = re_link.sub(
86
        lambda match: ' '.join(match.group(1).split(' ')[1:]), text)
87
88
    # Strip comments
89
    re_comment = re.compile('^#.*?$', re.MULTILINE)
90
    text = re_comment.sub('', text)
91
92
    return text
93
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
94
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
95
def parse_commands(content, command_names):
96
    """Extract indented commands from email body.
97
98
    All commands must be indented using either spaces or tabs.  They must be
99
    listed in command_names -- if not, they are silently ignored.
100
101
    The special command 'done' terminates processing.  It takes no arguments.
102
    Any commands that follow it will be ignored.  'done' should not be listed
103
    in command_names.
104
105
    While this syntax is the Launchpad standard, bug #29572 says it should be
106
    changed to only accept commands at the beginning and to not require
107
    indentation.
108
109
    A list of (command, args) tuples is returned.
110
    """
111
    commands = []
112
    for line in content.splitlines():
113
        # All commands have to be indented.
114
        if line.startswith(' ') or line.startswith('\t'):
115
            command_string = line.strip()
116
            if command_string == 'done':
117
                # If the 'done' statement is encountered,
118
                # stop reading any more commands.
119
                break
120
            words = command_string.split(' ')
7372.2.10 by Tim Penhey
Merge in RF and resolve conflicts.
121
            if len(words) > 0:
122
                first = words.pop(0)
123
                if first.endswith(':'):
124
                    first = first[:-1]
125
                if first in command_names:
126
                    commands.append((first, words))
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
127
    return commands
128
129
13668.1.15 by Curtis Hovey
Moved mail error messages to lp.code.mail.
130
def get_error_message(filename, error_templates=None, **interpolation_items):
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
131
    """Returns the error message that's in the given filename.
132
133
    If the error message requires some parameters, those are given in
134
    interpolation_items.
135
136
    The files are searched for in lib/canonical/launchpad/mail/errortemplates.
137
    """
13668.1.15 by Curtis Hovey
Moved mail error messages to lp.code.mail.
138
    if error_templates is None:
139
        error_templates = os.path.join(
140
            os.path.dirname(__file__), 'errortemplates')
141
    fullpath = os.path.join(error_templates, filename)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
142
    error_template = open(fullpath).read()
143
    return error_template % interpolation_items
144
145
7372.2.9 by Tim Penhey
More tests.
146
def get_person_or_team(person_name_or_email):
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
147
    """Get the `Person` from the vocabulary.
148
149
    :raises: EmailProcessingError if person not found.
150
    """
151
    valid_person_vocabulary = ValidPersonOrTeamVocabulary()
152
    try:
153
        person_term = valid_person_vocabulary.getTermByToken(
154
            person_name_or_email)
155
    except LookupError:
156
        raise EmailProcessingError(
157
            get_error_message(
158
                'no-such-person.txt',
159
                name_or_email=person_name_or_email))
160
    return person_term.value
161
162
7675.6.1 by Tim Penhey
Require merge directive emails to be signed.
163
def ensure_not_weakly_authenticated(signed_msg, context,
164
                                    error_template='not-signed.txt',
13668.1.24 by Curtis Hovey
Extracted ProcessMailLayer from test_system_documentation.
165
                                    no_key_template='key-not-registered.txt',
166
                                    error_templates=None):
11977.2.1 by Martin Pool
Accept DKIM authentication on mail to new@ as well as existing bugs (bug 643219).
167
    """Make sure that the current principal is not weakly authenticated.
168
169
    NB: While handling an email, the authentication state is stored partly in
170
    properties of the message object, and partly in the current security
171
    principal.  As a consequence this function will only work correctly if the
172
    message has just been passed through authenticateEmail -- you can't give
173
    it an arbitrary message object.
174
    """
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
175
    cur_principal = get_current_principal()
176
    # The security machinery doesn't know about
177
    # IWeaklyAuthenticatedPrincipal yet, so do a manual
178
    # check. Later we can rely on the security machinery to
179
    # cause Unauthorized errors.
180
    if IWeaklyAuthenticatedPrincipal.providedBy(cur_principal):
181
        if signed_msg.signature is None:
182
            error_message = get_error_message(
13668.1.24 by Curtis Hovey
Extracted ProcessMailLayer from test_system_documentation.
183
                error_template, error_templates=error_templates,
184
                context=context)
7372.2.1 by Tim Penhey
Refactoring the code and other mail handlers.
185
        else:
186
            import_url = canonical_url(
187
                getUtility(ILaunchBag).user) + '/+editpgpkeys'
188
            error_message = get_error_message(
13668.1.24 by Curtis Hovey
Extracted ProcessMailLayer from test_system_documentation.
189
                no_key_template, error_templates,
190
                import_url=import_url, context=context)
7372.2.8 by Tim Penhey
Add test for ensure_not_weakly_authenticated.
191
        raise IncomingEmailError(error_message)
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
192
193
11316.9.5 by Benji York
finish fixing QA failure caused by me misunderstanding the data model
194
def ensure_sane_signature_timestamp(timestamp, context,
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
195
                                    error_template='old-signature.txt'):
11977.2.1 by Martin Pool
Accept DKIM authentication on mail to new@ as well as existing bugs (bug 643219).
196
    """Ensure the signature was generated recently but not in the future.
197
198
    If the timestamp is wrong, the message is rejected rather than just
199
    treated as untrusted, so that the user gets a chance to understand
200
    the problem.  Therefore, this raises an error rather than returning
201
    a value.
202
203
    :param context: Short user-readable description of the place
204
        the problem occurred.
205
    :raises IncomingEmailError: if the timestamp is stale or implausible,
206
        containing a message based on the context and template.
207
    """
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
208
    fourty_eight_hours = 48 * 60 * 60
209
    ten_minutes = 10 * 60
210
    now = time.time()
211
    fourty_eight_hours_ago = now - fourty_eight_hours
212
    ten_minutes_in_the_future = now + ten_minutes
213
11316.9.5 by Benji York
finish fixing QA failure caused by me misunderstanding the data model
214
    if (timestamp < fourty_eight_hours_ago
215
            or timestamp > ten_minutes_in_the_future):
11316.9.1 by Benji York
add signature timestamp checking to GPG signed bug commands
216
        error_message = get_error_message(error_template, context=context)
217
        raise IncomingEmailError(error_message)
13174.1.1 by Martin Pool
Unify implementations of save-mail-to-librarian; both use uuids for file names
218
219
220
def save_mail_to_librarian(raw_mail):
221
    """Save the message to the librarian.
222
223
    It can be referenced from errors, and also accessed by code that needs to
224
    get back the exact original form.
225
    """
226
    # File the raw_mail in the Librarian.  We generate a filename to avoid
227
    # people guessing the URL.  We don't want URLs to private bug messages to
228
    # be guessable for example.
229
    file_name = str(uuid1()) + '.txt'
230
    file_alias = getUtility(ILibraryFileAliasSet).create(
231
            file_name,
232
            len(raw_mail),
233
            cStringIO(raw_mail), 'message/rfc822')
234
    return file_alias