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  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   
26 -class Message(object):
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
210 - def _normalize(self, addresslist):
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
221 - def _process(self):
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 -- &amp;, 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
322 - def __setattr__(self, name, value):
323 """Set the dirty flag as properties are updated.""" 324 325 self.__dict__[name] = value 326 if name != '_dirty': self.__dict__['_dirty'] = True
327
328 - def __call__(self):
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
364 -class KIDMessage(Message):
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
402 - def _process(self):
403 """Automatically generate the plain and rich text content.""" 404 405 #turbogears.view.base._load_engines() 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