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 |