1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Bug notification building code."""
__metaclass__ = type
__all__ = [
'BugNotificationBuilder',
'format_rfc2822_date',
'get_bugmail_error_address',
'get_bugmail_from_address',
]
from email.MIMEText import MIMEText
from email.Utils import formatdate
import rfc822
from zope.component import getUtility
from canonical.config import config
from canonical.launchpad.helpers import shortlist
from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.mail import format_address
def format_rfc2822_date(date):
"""Formats a date according to RFC2822's desires."""
return formatdate(rfc822.mktime_tz(date.utctimetuple() + (0, )))
def get_bugmail_from_address(person, bug):
"""Returns the right From: address to use for a bug notification."""
if person == getUtility(ILaunchpadCelebrities).janitor:
return format_address(
'Launchpad Bug Tracker',
"%s@%s" % (bug.id, config.launchpad.bugs_domain))
if person.hide_email_addresses:
return format_address(
person.displayname,
"%s@%s" % (bug.id, config.launchpad.bugs_domain))
if person.preferredemail is not None:
return format_address(person.displayname, person.preferredemail.email)
# XXX: Bjorn Tillenius 2006-04-05:
# The person doesn't have a preferred email set, but he
# added a comment (either via the email UI, or because he was
# imported as a deaf reporter). It shouldn't be possible to use the
# email UI if you don't have a preferred email set, but work around
# it for now by trying hard to find the right email address to use.
email_addresses = shortlist(
getUtility(IEmailAddressSet).getByPerson(person))
if not email_addresses:
# XXX: Bjorn Tillenius 2006-05-21 bug=33427:
# A user should always have at least one email address,
# but due to bug #33427, this isn't always the case.
return format_address(person.displayname,
"%s@%s" % (bug.id, config.launchpad.bugs_domain))
# At this point we have no validated emails to use: if any of the
# person's emails had been validated the preferredemail would be
# set. Since we have no idea of which email address is best to use,
# we choose the first one.
return format_address(person.displayname, email_addresses[0].email)
def get_bugmail_replyto_address(bug):
"""Return an appropriate bugmail Reply-To address.
:bug: the IBug.
:user: an IPerson whose name will appear in the From address, e.g.:
From: Foo Bar via Malone <123@bugs...>
"""
return u"Bug %d <%s@%s>" % (bug.id, bug.id, config.launchpad.bugs_domain)
def get_bugmail_error_address():
"""Return a suitable From address for a bug transaction error email."""
return config.malone.bugmail_error_from_address
class BugNotificationBuilder:
"""Constructs a MIMEText message for a bug notification.
Takes a bug and a set of headers and returns a new MIMEText
object. Common and expensive to calculate headers are cached
up-front.
"""
def __init__(self, bug, event_creator=None):
self.bug = bug
# Pre-calculate common headers.
self.common_headers = [
('Reply-To', get_bugmail_replyto_address(bug)),
('Sender', config.canonical.bounce_address),
]
# X-Launchpad-Bug
self.common_headers.extend(
('X-Launchpad-Bug', bugtask.asEmailHeaderValue())
for bugtask in bug.bugtasks)
# X-Launchpad-Bug-Tags
if len(bug.tags) > 0:
self.common_headers.append(
('X-Launchpad-Bug-Tags', ' '.join(bug.tags)))
# Add the X-Launchpad-Bug-Private header. This is a simple
# yes/no value denoting privacy for the bug.
if bug.private:
self.common_headers.append(
('X-Launchpad-Bug-Private', 'yes'))
else:
self.common_headers.append(
('X-Launchpad-Bug-Private', 'no'))
# Add the X-Launchpad-Bug-Security-Vulnerability header to
# denote security for this bug. This follows the same form as
# the -Bug-Private header.
if bug.security_related:
self.common_headers.append(
('X-Launchpad-Bug-Security-Vulnerability', 'yes'))
else:
self.common_headers.append(
('X-Launchpad-Bug-Security-Vulnerability', 'no'))
# Add the -Bug-Commenters header, a space-separated list of
# distinct IDs of people who have commented on the bug. The
# list is sorted to aid testing.
commenters = set(message.owner.name for message in bug.messages)
self.common_headers.append(
('X-Launchpad-Bug-Commenters', ' '.join(sorted(commenters))))
# Add the -Bug-Reporter header to identify the owner of the bug
# and the original bug task for filtering.
self.common_headers.append(
('X-Launchpad-Bug-Reporter',
'%s (%s)' % (bug.owner.displayname, bug.owner.name)))
# Add the -Bug-Modifier header to identify the person who
# modified the bug report.
if event_creator:
self.common_headers.append(
('X-Launchpad-Bug-Modifier',
'%s (%s)' % (event_creator.displayname,
event_creator.name)))
def build(self, from_address, to_address, body, subject, email_date,
rationale=None, references=None, message_id=None, filters=None):
"""Construct the notification.
:param from_address: The From address of the notification.
:param to_address: The To address for the notification.
:param body: The body text of the notification.
:type body: unicode
:param subject: The Subject of the notification.
:param email_date: The Date for the notification.
:param rationale: The rationale for why the recipient is
receiving this notification.
:param references: A value for the References header.
:param message_id: A value for the Message-ID header.
:return: An `email.MIMEText.MIMEText` object.
"""
message = MIMEText(body.encode('utf8'), 'plain', 'utf8')
message['Date'] = format_rfc2822_date(email_date)
message['From'] = from_address
message['To'] = to_address
# Add the common headers.
for header in self.common_headers:
message.add_header(*header)
if references is not None:
message['References'] = ' '.join(references)
if message_id is not None:
message['Message-Id'] = message_id
subject_prefix = "[Bug %d]" % self.bug.id
if subject is None:
message['Subject'] = subject_prefix
elif subject_prefix in subject:
message['Subject'] = subject
else:
message['Subject'] = "%s %s" % (subject_prefix, subject)
if rationale is not None:
message.add_header('X-Launchpad-Message-Rationale', rationale)
if filters is not None:
for filter in filters:
message.add_header(
'X-Launchpad-Subscription', filter)
return message
|