1
2
3 """MIME-encoded electronic mail message classes."""
4
5 from turbomail import release
6
7 import turbogears, re, os, email
8
9 import email.Message
10 from email import Encoders, Charset
11 from email.Message import Message as MIMEMessage
12 from email.Utils import formatdate
13 from email.MIMEMultipart import MIMEMultipart
14 from email.MIMEBase import MIMEBase
15 from email.MIMEText import MIMEText
16 from email.Header import Header
17
18 import logging
19 log = logging.getLogger("turbomail.message")
20
21
22 __all__ = ['Message', 'KIDMessage']
23 _rich_to_plain = re.compile(r"(<[^>]+>)")
24
25
27 """Simple e-mail message class.
28
29 Message provides a means to easily create e-mail messages to be
30 sent through the Dispatch mechanism or MailPool. Message provides
31 various helper functions to correctly format plain text, dual plain
32 text and rich text MIME encoded messages, as well as handle
33 embedded and external attachments.
34
35 All properties can be set from the constructor.
36
37 Example usage::
38
39 import turbomail
40 message = turbomail.Message(
41 "from@host.com",
42 "to@host.com",
43 "Subject",
44 plain="This is a plain message."
45 )
46
47 E-mail addresses can be represented as any of the following:
48 - A string.
49 - A 2-tuple of ("Full Name", "name@host.tld")
50
51 Encoding can be overridden on a per-message basis, but note that
52 'utf-8-qp' modifies the default 'utf-8' behaviour to output
53 quoted-printable, and you will have to change it back yourself if
54 you want base64 encoding.
55
56 @ivar _processed: Has the MIME-encoded message been generated?
57 @type _processed: bool
58 @ivar _dirty: Has there been changes since the MIME message was last
59 generated?
60 @type _dirty: bool
61 @ivar date: The Date header. Must be correctly formatted.
62 @type date: string
63 @ivar recipient: The To header. A string, 2-tuple, or list of
64 strings or 2-tuples.
65 @ivar sender: The From header. A string or 2-tuple.
66 @ivar organization: The Organization header. I{Optional.}
67 @type organization: string
68 @ivar replyto: The X-Reply-To header. A string or 2-tuple.
69 I{Optional.}
70 @ivar disposition: The Disposition-Notification-To header. A string
71 or 2-tuple. I{Optional.}
72 @ivar cc: The CC header. As per the recipient property.
73 I{Optional.}
74 @ivar bcc: The BCC header. As per the recipient property.
75 I{Optional.}
76 @ivar encoding: Content encoding. Pulled from I{mail.encoding},
77 defaults to 'us-ascii'.
78 @type encoding: string
79 @ivar priority: The X-Priority header, a number ranging from 1-5.
80 I{Optional.} Default: B{3}
81 @type priority: int
82 @ivar subject: The Subject header.
83 @type subject: string
84 @ivar plain: The plain text content of the message.
85 @type plain: string
86 @ivar rich: The rich text (HTML) content of the message. Plain text
87 content B{must} be available as well.
88 @type rich: string
89 @ivar attachments: A list of MIME-encoded attachments.
90 @type attachments: list
91 @ivar embedded: A list of MIME-encoded embedded obejects for use in
92 the text/html part.
93 @type embedded: list
94 @ivar headers: A list of additional headers. Can be added in a wide
95 variety of formats: a list of strings, list of
96 tuples, a dictionary, etc. Look at the code.
97 @ivar smtpfrom: The envelope address, if different than the sender.
98 """
99
100 - def __init__(self, sender=None, recipient=None, subject=None, **kw):
101 """Instantiate a new Message object.
102
103 No arguments are required, as everything can be set using class
104 properties. Alternatively, I{everything} can be set using the
105 constructor, using named arguments. The first three positional
106 arguments can be used to quickly prepare a simple message.
107
108 An instance of Message is callable.
109
110 @param sender: The e-mail address of the sender. This is
111 encoded as the "From:" SMTP header.
112 @type sender: string
113
114 @param recipient: The recipient of the message. This gets
115 encoded as the "To:" SMTP header.
116 @type recipient: string
117
118 @param subject: The subject of the message. This gets encoded
119 as the "Subject:" SMTP header.
120 @type subject: string
121 """
122
123 super(Message, self).__init__()
124
125 self._processed = False
126 self._dirty = False
127
128 self.date = formatdate(localtime=True)
129 self.recipient = recipient
130 self.sender = sender
131 self.organization = None
132 self.replyto = None
133 self.disposition = None
134 self.cc = []
135 self.bcc = []
136 self.encoding = turbogears.config.get("mail.encoding", 'us-ascii')
137 self.priority = 3
138 self.subject = subject
139 self.plain = None
140 self.rich = None
141 self.attachments = []
142 self.embedded = []
143 self.headers = {}
144 self.smtpfrom = None
145
146 for i, j in kw.iteritems():
147 assert i in self.__dict__, "Unknown attribute: '%s'" % i
148 self.__dict__[i] = j
149
150 - def attach(self, file, name=None):
151 """Attach an on-disk file to this message.
152
153 @param file: The path to the file you wish to attach, or an
154 instance of a file-like object.
155
156 @param name: You can optionally override the filename of the
157 attached file. This name will appear in the
158 recipient's mail viewer. B{Optional if passing
159 an on-disk path. Required if passing a file-like
160 object.}
161 @type name: string
162 """
163
164 part = MIMEBase('application', "octet-stream")
165
166 if isinstance(file, (str, unicode)):
167 fp = open(file, "rb")
168 else:
169 assert name is not None, "If attaching a file-like object, you must pass a custom filename."
170 fp = file
171
172 part.set_payload(fp.read())
173 Encoders.encode_base64(part)
174
175 part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None]))
176
177 self.attachments.append(part)
178
179 - def embed(self, file, name):
180 """Attach an on-disk image file and prepare for HTML embedding.
181
182 This method should only be used to embed images.
183
184 @param file: The path to the file you wish to attach, or an
185 instance of a file-like object.
186
187 @param name: You can optionally override the filename of the
188 attached file. This name will appear in the
189 recipient's mail viewer. B{Optional if passing
190 an on-disk path. Required if passing a file-like
191 object.}
192 @type name: string
193 """
194
195 from email.MIMEImage import MIMEImage
196
197 if isinstance(file, (str, unicode)):
198 fp = open(file, "rb")
199 else:
200 assert name is not None, "If embedding a file-like object, you must pass a custom filename."
201 fp = file
202
203 part = MIMEImage(fp.read())
204 fp.close()
205
206 part.add_header('Content-ID', '<%s>' % name)
207
208 self.embedded.append(part)
209
211 """A utility function to return a list of addresses as a string."""
212
213 addresses = []
214 for i in [[addresslist], addresslist][type(addresslist) == type([])]:
215 if type(i) == type(()):
216 addresses.append('"%s" <%s>' % (str(Header(i[0])), i[1]))
217 else: addresses.append(i)
218
219 return ",\n ".join(addresses)
220
222 """Produce the final MIME message.
223
224 Additionally, if only a rich text part exits, strip the HTML to
225 produce the plain text part. (This produces identical output as
226 KID, although lacks reverse entity conversion -- &, etc.)
227 """
228
229 if self.encoding == 'utf-8-qp':
230 Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
231 self.encoding = 'utf-8'
232
233 if callable(self.plain):
234 self.plain = self.plain()
235
236 if callable(self.rich):
237 self.rich = self.rich()
238
239 if self.rich and not self.plain:
240 self.plain = _rich_to_plain.sub('', self.rich)
241
242 if not self.rich:
243 if not self.attachments:
244 message = MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding)
245
246 else:
247 message = MIMEMultipart()
248 message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
249
250 else:
251 if not self.attachments:
252 message = MIMEMultipart('alternative')
253 message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
254
255 if not self.embedded:
256 message.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
257 else:
258 related = MIMEMultipart('related')
259 message.attach(related)
260 related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
261
262 for attachment in self.embedded:
263 related.attach(attachment)
264
265 else:
266 message = MIMEMultipart()
267 alternative = MIMEMultipart('alternative')
268 message.attach(alternative)
269
270 alternative.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
271
272 if not self.embedded:
273 alternative.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
274 else:
275 related = MIMEMultipart('related')
276 alternative.attach(related)
277 related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
278
279 for attachment in self.embedded:
280 related.attach(attachment)
281
282 for attachment in self.attachments:
283 message.attach(attachment)
284
285 message.add_header('From', self._normalize(self.sender))
286 message.add_header('Subject', self.subject)
287 message.add_header('Date', formatdate(localtime=True))
288 message.add_header('To', self._normalize(self.recipient))
289 if self.replyto: message.add_header('Reply-To', self._normalize(self.replyto))
290 if self.cc: message.add_header('Cc', self._normalize(self.cc))
291 if self.disposition: message.add_header('Disposition-Notification-To', self._normalize(self.disposition))
292 if self.organization: message.add_header('Organization', self.organization)
293 if self.priority != 3: message.add_header('X-Priority', self.priority)
294
295 if not self.smtpfrom:
296 if type(self.sender) == type([]) and len(self.sender) > 1:
297 message.add_header('Sender', self._normalize(self.sender[0]))
298 else:
299 message.add_header('Sender', self._normalize(self.smtpfrom))
300
301 message.add_header('X-Mailer', "TurboMail TurboGears Extension v.%s" % release.version)
302
303 if type(self.headers) in [type(()), type([])]:
304 for header in self.headers:
305 if type(header) in [type(()), type([])]:
306 message.add_header(*header)
307 elif type(header) == type({}):
308 message.add_header(**header)
309 elif type(self.headers) == type({}):
310 for name, header in self.headers.iteritems():
311 if type(header) in [type(()), type([])]:
312 message.add_header(name, *header)
313 elif type(header) == type({}):
314 message.add_header(name, **header)
315 else:
316 message.add_header(name, header)
317
318 self._message = message
319 self._processed = True
320 self._dirty = False
321
323 """Set the dirty flag as properties are updated."""
324
325 self.__dict__[name] = value
326 if name != '_dirty': self.__dict__['_dirty'] = True
327
329 """Produce a valid MIME-encoded message and return valid input
330 for the Dispatch class to process.
331
332 @return: Returns a tuple containing sender and recipient e-mail
333 addresses and the string output of MIMEMultipart.
334 @rtype: tuple
335 """
336
337 if not self._processed or self._dirty:
338 self._process()
339
340 recipients = []
341
342 if isinstance(self.recipient, list):
343 recipients.extend(self.recipient)
344 else: recipients.append(self.recipient)
345
346 if isinstance(self.cc, list):
347 recipients.extend(self.cc)
348 else: recipients.append(self.cc)
349
350 if isinstance(self.bcc, list):
351 recipients.extend(self.bcc)
352 else: recipients.append(self.bcc)
353
354 return dict(
355 sender=self.sender,
356 smtpfrom = self.smtpfrom,
357 to=[[self.recipient], self.recipient][isinstance(self.recipient, list)],
358 recipients=[i[1] for i in recipients if isinstance(i, tuple)] + [i for i in recipients if not isinstance(i, tuple)],
359 subject=self.subject,
360 message=self._message.as_string(),
361 )
362
363
365 """A message that accepts a named template with arguments.
366
367 Example usage::
368
369 import turbomail
370 message = turbomail.KIDMessage(
371 "from@host.com",
372 "to@host.com",
373 "Subject",
374 "app.templates.mail",
375 dict()
376 )
377
378 Do not specify message.plain or message.rich content - the template
379 will override what you set. If you wish to hand-produce content,
380 use the Message class.
381 """
382
383 - def __init__(self, sender, recipient, subject, template, variables={}, **kw):
384 """Store the additional template and variable information.
385
386 @param template: A dot-path to a valid KID template.
387 @type template: string
388
389 @param variables: A dictionary containing named variables to
390 pass to the template engine.
391 @type variables: dict
392 """
393
394 log.warn("Use of KIDMessage is deprecated and will be removed in version 2.1.")
395
396 self._template = template
397 self._variables = dict(sender=sender, recipient=recipient, subject=subject)
398 self._variables.update(variables)
399
400 super(KIDMessage, self).__init__(sender, recipient, subject, **kw)
401
403 """Automatically generate the plain and rich text content."""
404
405
406
407 data = dict()
408
409 for (i, j) in self._variables.iteritems():
410 if callable(j): data[i] = j()
411 else: data[i] = j
412
413 self.plain = turbogears.view.engines.get('kid').render(data, format="plain", template=self._template)
414 self.rich = turbogears.view.engines.get('kid').render(data, template=self._template)
415
416 return super(KIDMessage, self)._process()
417