1
2
3 """MIME-encoded electronic mail message classes."""
4
5
6 import logging
7 import os
8 import time
9 import warnings
10
11 from datetime import datetime
12
13 from turbomail import release
14 from turbomail.compat import Header, MIMEBase, MIMEImage, MIMEMultipart, MIMEText, encode_base64, formatdate, make_msgid
15 from turbomail.util import Address, AddressList
16 from turbomail.control import interface
17
18
19 __all__ = ['Message']
20
21 log = logging.getLogger("turbomail.message")
22
23
24
27
28
30 - def __init__(self, smtp_from=None, to=None, kw=None):
35
36 smtp_from = AddressList.protected('_smtp_from')
37 to = AddressList.protected('_to')
38
40 if value not in [None, '']:
41 kw[name] = value
42
43 - def kwpop(self, kw, name, configkey=None, default=None, old_configkey=None):
44 if name in kw:
45 value = kw.pop(name)
46 else:
47 if configkey == None:
48 configkey = 'mail.message.%s' % name
49 value = interface.config.get(configkey, NoDefault)
50
51 if value == NoDefault and old_configkey != None:
52 value = interface.config.get(old_configkey, NoDefault)
53 if value != NoDefault:
54 msg = 'Falling back to deprecated configuration option "%s", please use "%s" instead'
55 warnings.warn(msg % (old_configkey, configkey),
56 category=DeprecationWarning)
57 if value == NoDefault:
58 value = default
59 return value
60
62 deprecated_value = self.kwpop(kw, deprecated_name)
63 if deprecated_value != None:
64 self._warn_about_deprecated_property(deprecated_name, new_name)
65 value = deprecated_value
66 elif value == None:
67 value = self.kwpop(kw, new_name)
68 return value
69
72
73
74
76 msg = 'Property "%s" is deprecated, please use "%s" instead'
77 warnings.warn(msg % (deprecated_name, new_name), category=DeprecationWarning)
78
82
86 smtpfrom = property(fget=get_smtpfrom, fset=set_smtpfrom)
87
91
95 recipient = property(fget=get_recipient, fset=set_recipient)
96
97
99 """Simple e-mail message class."""
100
101 - def __init__(self, author=None, to=None, subject=None, **kw):
102 """Instantiate a new Message object.
103
104 No arguments are required, as everything can be set using class
105 properties. Alternatively, I{everything} can be set using the
106 constructor, using named arguments. The first three positional
107 arguments can be used to quickly prepare a simple message.
108 """
109 super(Message, self).__init__(to=to, kw=kw)
110
111 kwpop = lambda *args, **kwargs: self.kwpop(kw, *args, **kwargs)
112 pop_deprecated = lambda name, old_name: self.pop_deprecated(kw, name, old_name)
113
114 self.merge_if_set(kw, author, 'author')
115 self._author = AddressList(kwpop('author'))
116 self._cc = AddressList(kwpop("cc"))
117 self._bcc = AddressList(kwpop("bcc"))
118 self._sender = AddressList(kwpop("sender"))
119 self._reply_to = AddressList(pop_deprecated('reply_to', 'replyto'))
120 self._disposition = AddressList(kwpop("disposition"))
121
122 self.subject = subject
123 self.date = kwpop("date")
124
125 self.encoding = kwpop("encoding", default='us-ascii',
126 old_configkey='mail.encoding')
127
128 self.organization = kwpop("organization")
129 self.priority = kwpop("priority")
130
131 self.plain = kwpop("plain", default=None)
132 self.rich = kwpop("rich", default=None)
133 self.attachments = kwpop("attachments", default=[])
134 self.embedded = kwpop("embedded", default=[])
135 self.headers = kwpop("headers", default=[])
136
137 self._id = kw.get("id", None)
138
139 self._processed = False
140 self._dirty = False
141 if len(kw) > 0:
142 parameter_name = kw.keys()[0]
143 error_msg = "__init__() got an unexpected keyword argument '%s'"
144 raise TypeError(error_msg % parameter_name)
145
146 author = AddressList.protected('_author')
147 bcc = AddressList.protected('_bcc')
148 cc = AddressList.protected('_cc')
149 disposition = AddressList.protected('_disposition')
150 reply_to = AddressList.protected('_reply_to')
151 sender = AddressList.protected('_sender')
152
154 """Set the dirty flag as properties are updated."""
155 super(Message, self).__setattr__(name, value)
156 if name not in ('bcc', '_dirty', '_processed'):
157 self.__dict__['_dirty'] = True
158
160 return self.mime.as_string()
161
163
164
165
166 return self.author
167
170 authors = property(_get_authors, _set_authors)
171
173 if not self._id or (self._processed and self._dirty):
174 self.__dict__['_id'] = make_msgid()
175 self._processed = False
176 return self._id
177 id = property(id)
178
192 envelope_sender = property(envelope_sender)
193
196 recipients = property(recipients)
197
199 if not rich:
200 message = plain
201
202 else:
203 message = MIMEMultipart('alternative')
204 message.attach(plain)
205
206 if not self.embedded:
207 message.attach(rich)
208
209 else:
210 embedded = MIMEMultipart('related')
211 embedded.attach(rich)
212 for attachment in self.embedded: embedded.attach(attachment)
213 message.attach(embedded)
214
215 if self.attachments:
216 attachments = MIMEMultipart()
217 attachments.attach(message)
218 for attachment in self.attachments: attachments.attach(attachment)
219 message = attachments
220
221 return message
222
224 """Gets the date_value (may be None, basestring, float or
225 datetime.datetime instance) and returns a valid date string as per
226 RFC 2822."""
227 if isinstance(date_value, datetime):
228 date_value = time.mktime(date_value.timetuple())
229 if not isinstance(date_value, basestring):
230 date_value = formatdate(date_value, localtime=True)
231 return date_value
232
234 date_value = self._build_date_header_string(self.date)
235 headers = [
236 ('Sender', sender),
237 ('From', author),
238 ('Reply-To', self.reply_to),
239 ('Subject', self.subject),
240 ('Date', date_value),
241 ('To', self.to),
242 ('Cc', self.cc),
243 ('Disposition-Notification-To', self.disposition),
244 ('Organization', self.organization),
245 ('X-Priority', self.priority),
246 ]
247
248 if interface.config.get("mail.brand", True):
249 headers.extend([
250 ('X-Mailer', "%s %s" % (release.name, release.version)),
251 ])
252 if isinstance(self.headers, dict):
253 for key in self.headers:
254 headers.append((key, self.headers[key]))
255 else:
256 headers.extend(self.headers)
257 return headers
258
260 for header in headers:
261 if isinstance(header, (tuple, list)):
262 if header[1] is None or ( isinstance(header[1], list) and not header[1] ): continue
263 header = list(header)
264 if isinstance(header[1], unicode):
265 header[1] = Header(header[1], self.encoding)
266 elif isinstance(header[1], AddressList):
267 header[1] = header[1].encode(self.encoding)
268 header[1] = str(header[1])
269 message.add_header(*header)
270 elif isinstance(header, dict):
271 message.add_header(**header)
272
274 """Produce the final MIME message."""
275 author = self.author
276 sender = self.sender
277 if not author and sender:
278 msg = 'Please specify the author using the "author" property. ' + \
279 'Using "sender" for the From header is deprecated!'
280 warnings.warn(msg, category=DeprecationWarning)
281 author = sender
282 sender = []
283 if not author:
284 raise ValueError('You must specify an author.')
285
286 assert self.subject, "You must specify a subject."
287 assert len(self.recipients) > 0, "You must specify at least one recipient."
288 assert self.plain, "You must provide plain text content."
289
290 if len(author) > 1 and len(sender) == 0:
291 raise ValueError('If there are multiple authors of message, you must specify a sender!')
292 if len(sender) > 1:
293 raise ValueError('You must not specify more than one sender!')
294
295 if not self._dirty and self._processed and not interface.config.get("mail.debug", False):
296 return self._mime
297
298 self._processed = False
299
300 plain = MIMEText(self._callable(self.plain).encode(self.encoding), 'plain', self.encoding)
301
302 rich = None
303 if self.rich:
304 rich = MIMEText(self._callable(self.rich).encode(self.encoding), 'html', self.encoding)
305
306 message = self.mime_document(plain, rich)
307 headers = self._build_header_list(author, sender)
308 self._add_headers_to_message(message, headers)
309
310 self._mime = message
311 self._processed = True
312 self._dirty = False
313
314 return message
315 mime = property(mime)
316
317 - def attach(self, file, name=None):
318 """Attach an on-disk file to this message."""
319
320 part = MIMEBase('application', "octet-stream")
321
322 if isinstance(file, (str, unicode)):
323 fp = open(file, "rb")
324 else:
325 assert name is not None, "If attaching a file-like object, you must pass a custom filename, as one can not be inferred."
326 fp = file
327
328 part.set_payload(fp.read())
329 encode_base64(part)
330
331 part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None]))
332
333 self.attachments.append(part)
334
335 - def embed(self, file, name=None):
336 """Attach an on-disk image file and prepare for HTML embedding.
337
338 This method should only be used to embed images.
339
340 @param file: The path to the file you wish to attach, or an
341 instance of a file-like object.
342
343 @param name: You can optionally override the filename of the
344 attached file. This name will appear in the
345 recipient's mail viewer. B{Optional if passing
346 an on-disk path. Required if passing a file-like
347 object.}
348 @type name: string
349 """
350
351 if isinstance(file, (str, unicode)):
352 fp = open(file, "rb")
353 name = os.path.basename(file)
354 else:
355 assert name is not None, "If embedding a file-like object, you must pass a custom filename."
356 fp = file
357
358 part = MIMEImage(fp.read(), name=name)
359 fp.close()
360
361 del part['Content-Disposition']
362 part.add_header('Content-Disposition', 'inline', filename=name)
363 part.add_header('Content-ID', '<%s>' % name)
364
365 self.embedded.append(part)
366
368 if callable(var):
369 return var()
370 return var
371
372
373
374
378
382 replyto = property(fget=get_replyto, fset=set_replyto)
383