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
201
202
203
204
205
206
|
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Base class for sending out emails."""
__metaclass__ = type
__all__ = ['BaseMailer', 'RecipientReason']
import logging
from smtplib import SMTPException
from canonical.launchpad.helpers import get_email_template
from lp.services.mail.notificationrecipientset import (
NotificationRecipientSet)
from lp.services.mail.sendmail import (
append_footer, format_address, MailController
)
from lp.services.utils import text_delta
class BaseMailer:
"""Base class for notification mailers.
Subclasses must provide getReason (or reimplement _getTemplateParameters
or generateEmail).
It is expected that subclasses may override _getHeaders,
_getTemplateParams, and perhaps _getBody.
"""
def __init__(self, subject, template_name, recipients, from_address,
delta=None, message_id=None, notification_type=None,
mail_controller_class=None):
"""Constructor.
:param subject: A Python dict-replacement template for the subject
line of the email.
:param template: Name of the template to use for the message body.
:param recipients: A dict of recipient to Subscription.
:param from_address: The from_address to use on emails.
:param delta: A Delta object with members "delta_values", "interface"
and "new_values", such as BranchMergeProposalDelta.
:param message_id: The Message-Id to use for generated emails. If
not supplied, random message-ids will be used.
:param mail_controller_class: The class of the mail controller to
use to send the mails. Defaults to `MailController`.
"""
self._subject_template = subject
self._template_name = template_name
self._recipients = NotificationRecipientSet()
for recipient, reason in recipients.iteritems():
self._recipients.add(recipient, reason, reason.mail_header)
self.from_address = from_address
self.delta = delta
self.message_id = message_id
self.notification_type = notification_type
self.logger = logging.getLogger('lp.services.mail.basemailer')
if mail_controller_class is None:
mail_controller_class = MailController
self._mail_controller_class = mail_controller_class
def _getToAddresses(self, recipient, email):
return [format_address(recipient.displayname, email)]
def generateEmail(self, email, recipient, force_no_attachments=False):
"""Generate the email for this recipient.
:param email: Email address of the recipient to send to.
:param recipient: The Person to send to.
:return: (headers, subject, body) of the email.
"""
to_addresses = self._getToAddresses(recipient, email)
headers = self._getHeaders(email)
subject = self._getSubject(email)
body = self._getBody(email)
ctrl = self._mail_controller_class(
self.from_address, to_addresses, subject, body, headers,
envelope_to=[email])
if force_no_attachments:
ctrl.addAttachment(
'Excessively large attachments removed.',
content_type='text/plain', inline=True)
else:
self._addAttachments(ctrl, email)
return ctrl
def _getSubject(self, email):
"""The subject template expanded with the template params."""
return self._subject_template % self._getTemplateParams(email)
def _getReplyToAddress(self):
"""Return the address to use for the reply-to header."""
return None
def _getHeaders(self, email):
"""Return the mail headers to use."""
reason, rationale = self._recipients.getReason(email)
headers = {'X-Launchpad-Message-Rationale': reason.mail_header}
if self.notification_type is not None:
headers['X-Launchpad-Notification-Type'] = self.notification_type
reply_to = self._getReplyToAddress()
if reply_to is not None:
headers['Reply-To'] = reply_to
if self.message_id is not None:
headers['Message-Id'] = self.message_id
return headers
def _addAttachments(self, ctrl, email):
"""Add any appropriate attachments to a MailController.
Default implementation does nothing.
:param ctrl: The MailController to add attachments to.
:param email: The email address of the recipient.
"""
pass
def _getTemplateParams(self, email):
"""Return a dict of values to use in the body and subject."""
reason, rationale = self._recipients.getReason(email)
params = {'reason': reason.getReason()}
if self.delta is not None:
params['delta'] = self.textDelta()
return params
def textDelta(self):
"""Return a textual version of the class delta."""
return text_delta(self.delta, self.delta.delta_values,
self.delta.new_values, self.delta.interface)
def _getBody(self, email):
"""Return the complete body to use for this email."""
template = get_email_template(self._template_name)
params = self._getTemplateParams(email)
body = template % params
footer = self._getFooter(params)
if footer is not None:
body = append_footer(body, footer)
return body
def _getFooter(self, params):
"""Provide a footer to attach to the body, or None."""
return None
def sendAll(self):
"""Send notifications to all recipients."""
# We never want SMTP errors to propagate from this function.
for email, recipient in self._recipients.getRecipientPersons():
try:
ctrl = self.generateEmail(email, recipient)
ctrl.send()
except SMTPException, e:
# If the initial sending failed, try again without
# attachments.
try:
ctrl = self.generateEmail(
email, recipient, force_no_attachments=True)
ctrl.send()
except SMTPException, e:
# Don't want an entire stack trace, just some details.
self.logger.warning(
'send failed for %s, %s' % (email, e))
class RecipientReason:
"""Reason for sending mail to a recipient."""
def __init__(self, subscriber, recipient, mail_header, reason_template):
self.subscriber = subscriber
self.recipient = recipient
self.mail_header = mail_header
self.reason_template = reason_template
@staticmethod
def makeRationale(rationale_base, person):
if person.is_team:
return '%s @%s' % (rationale_base, person.name)
else:
return rationale_base
def _getTemplateValues(self):
template_values = {
'entity_is': 'You are',
'lc_entity_is': 'you are',
}
if self.recipient != self.subscriber:
assert self.recipient.hasParticipationEntryFor(self.subscriber), (
'%s does not participate in team %s.' %
(self.recipient.displayname, self.subscriber.displayname))
if self.recipient != self.subscriber or self.subscriber.is_team:
template_values['entity_is'] = (
'Your team %s is' % self.subscriber.displayname)
template_values['lc_entity_is'] = (
'your team %s is' % self.subscriber.displayname)
return template_values
def getReason(self):
"""Return a string explaining why the recipient is a recipient."""
return (self.reason_template % self._getTemplateValues())
@classmethod
def forBuildRequester(cls, requester):
header = cls.makeRationale('Requester', requester)
reason = '%(entity_is)s the requester of the build.'
return cls(requester, requester, header, reason)
|