Package turbomail :: Module message
[hide private]
[frames] | no frames]

Source Code for Module turbomail.message

  1  # encoding: utf-8 
  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   
25 -class NoDefault(object):
26 pass
27 28
29 -class BaseMessage(object):
30 - def __init__(self, smtp_from=None, to=None, kw=None):
31 self.merge_if_set(kw, smtp_from, 'smtp_from') 32 self._smtp_from = AddressList(self.pop_deprecated(kw, 'smtp_from', 'smtpfrom')) 33 self._to = AddressList(self.pop_deprecated(kw, 'to', 'recipient', value=to)) 34 self.nr_retries = self.kwpop(kw, 'nr_retries', default=3)
35 36 smtp_from = AddressList.protected('_smtp_from') 37 to = AddressList.protected('_to') 38
39 - def merge_if_set(self, kw, value, name):
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
61 - def pop_deprecated(self, kw, new_name, deprecated_name, value=None):
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
70 - def send(self):
71 return interface.send(self)
72 73 # -------------------------------------------------------------------------- 74 # Deprecated properties
75 - def _warn_about_deprecated_property(self, deprecated_name, new_name):
76 msg = 'Property "%s" is deprecated, please use "%s" instead' 77 warnings.warn(msg % (deprecated_name, new_name), category=DeprecationWarning)
78
79 - def get_smtpfrom(self):
80 self._warn_about_deprecated_property('smtpfrom', 'smtp_from') 81 return self.smtp_from
82
83 - def set_smtpfrom(self, smtpfrom):
84 self._warn_about_deprecated_property('smtpfrom', 'smtp_from') 85 self.smtp_from = smtpfrom
86 smtpfrom = property(fget=get_smtpfrom, fset=set_smtpfrom) 87
88 - def get_recipient(self):
89 self._warn_about_deprecated_property('recipient', 'to') 90 return self.to
91
92 - def set_recipient(self, recipient):
93 self._warn_about_deprecated_property('recipient', 'to') 94 self.to = recipient
95 recipient = property(fget=get_recipient, fset=set_recipient)
96 97
98 -class Message(BaseMessage):
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
153 - def __setattr__(self, name, value):
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
159 - def __str__(self):
160 return self.mime.as_string()
161
162 - def _get_authors(self):
163 # Just for better readability. I put this method here because a wrapped 164 # message must only have one author (smtp_from) so this method is only 165 # useful in a TurboMail Message. 166 return self.author
167
168 - def _set_authors(self, value):
169 self.author = value
170 authors = property(_get_authors, _set_authors) 171
172 - def id(self):
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
179 - def envelope_sender(self):
180 """Returns the address of the envelope sender address (SMTP from, if not 181 set the sender, if this one isn't set too, the author).""" 182 envelope_sender = None 183 # TODO: Make this check better as soon as SMTP from and sender are 184 # Addresses, not AddressLists anymore. 185 if self.smtp_from != None and len(self.smtp_from) > 0: 186 envelope_sender = self.smtp_from 187 elif self.sender != None and len(self.sender) > 0: 188 envelope_sender = self.sender 189 else: 190 envelope_sender = self.author 191 return Address(envelope_sender)
192 envelope_sender = property(envelope_sender) 193
194 - def recipients(self):
195 return AddressList(self.to + self.cc + self.bcc)
196 recipients = property(recipients) 197
198 - def mime_document(self, plain, rich=None):
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
223 - def _build_date_header_string(self, date_value):
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
233 - def _build_header_list(self, author, sender):
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
259 - def _add_headers_to_message(self, message, headers):
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
273 - def mime(self):
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
367 - def _callable(self, var):
368 if callable(var): 369 return var() 370 return var
371 372 # -------------------------------------------------------------------------- 373 # Deprecated properties 374
375 - def get_replyto(self):
376 self._warn_about_deprecated_property('replyto', 'reply_to') 377 return self.reply_to
378
379 - def set_replyto(self, replyto):
380 self._warn_about_deprecated_property('replyto', 'reply_to') 381 self.reply_to = replyto
382 replyto = property(fget=get_replyto, fset=set_replyto)
383