| 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. |
| 147 | | assert i in self.__dict__, "Unknown attribute: '%s'" % i |
| 148 | | self.__dict__[i] = j |
| | 74 | assert hasattr(self, i), "Unknown attribute: '%s'" % i |
| | 75 | setattr(self, i, j) |
| | 76 | |
| | 77 | def __setattr__(self, name, value): |
| | 78 | """Set the dirty flag as properties are updated.""" |
| | 79 | |
| | 80 | super(Message, self).__setattr__(name, value) |
| | 81 | |
| | 82 | if name not in ('bcc', '_dirty'): self.__dict__['_dirty'] = True |
| | 83 | |
| | 84 | def __str__(self): |
| | 85 | return self.mime.as_string() |
| | 86 | |
| | 87 | sender = property(lambda self: self._sender, lambda self, value: self._sender.replace(value), lambda self: self._sender.replace()) |
| | 88 | envelope = property(lambda self: self._envelope, lambda self, value: self._envelope.replace(value), lambda self: self._envelope.replace()) |
| | 89 | reply = property(lambda self: self._reply, lambda self, value: self._reply.replace(value), lambda self: self._reply.replace()) |
| | 90 | to = property(lambda self: self._to, lambda self, value: self._to.replace(value), lambda self: self._to.replace()) |
| | 91 | cc = property(lambda self: self._cc, lambda self, value: self._cc.replace(value), lambda self: self._cc.replace()) |
| | 92 | bcc = property(lambda self: self._bcc, lambda self, value: self._bcc.replace(value), lambda self: self._bcc.replace()) |
| | 93 | disposition = property(lambda self: self._disposition, lambda self, value: self._disposition.replace(value), lambda self: self._disposition.replace()) |
| | 94 | |
| | 95 | @property |
| | 96 | def recipients(self): |
| | 97 | return self.to + self.cc + self.bcc |
| | 98 | |
| | 99 | @property |
| | 100 | def mime(self): |
| | 101 | """Produce the final MIME message.""" |
| | 102 | |
| | 103 | assert self.sender, "You must specify a sender." |
| | 104 | assert self.subject, "You must specify a subject." |
| | 105 | assert self.to or self.cc or self.bcc, "You must specify at least one recipient." |
| | 106 | assert self.plain, "You must provide plain text content." |
| | 107 | |
| | 108 | #if not self._dirty and self._processed: |
| | 109 | # return self._mime |
| | 110 | |
| | 111 | plain = MIMEText(self._callable(self.plain).encode(self.encoding), 'plain', self.encoding) |
| | 112 | rich = self.rich and MIMEText(self._callable(self.rich).encode(self.encoding), 'html', self.encoding) or None |
| | 113 | |
| | 114 | def generate_mime(): |
| | 115 | if not rich: return plain |
| | 116 | |
| | 117 | message = MIMEMultipart('alternative') |
| | 118 | message.attach(plain) |
| | 119 | |
| | 120 | if not self.embedded: |
| | 121 | message.attach(rich) |
| | 122 | |
| | 123 | else: |
| | 124 | embedded = MIMEMultipart('related') |
| | 125 | embedded.attach(rich) |
| | 126 | for attachment in self.embedded: embedded.attach(attachment) |
| | 127 | message.attach(embedded) |
| | 128 | |
| | 129 | return message |
| | 130 | |
| | 131 | message = generate_mime() |
| | 132 | |
| | 133 | if self.attachments: |
| | 134 | attachments = MIMEMultipart() |
| | 135 | attachments.attach(message) |
| | 136 | for attachment in self.attachments: attachments.attach(attachment) |
| | 137 | message = attachments |
| | 138 | |
| | 139 | headers = [ |
| | 140 | ('Sender', self.envelope and self.envelope or AddressList(self.sender[0])), # AddressList |
| | 141 | ('From', self.sender), # AddressList |
| | 142 | ('Reply-To', self.reply), # AddressList |
| | 143 | ('Subject', self.subject), |
| | 144 | ('Date', self.date), |
| | 145 | ('To', self.to), # AddressList |
| | 146 | ('Cc', self.cc), # AddressList |
| | 147 | ('Disposition-Notification-To', self.disposition), # AddressList |
| | 148 | ('Organization', self.organization), |
| | 149 | ('X-Priority', self.priority), |
| | 150 | ('X-Mailer', "TurboMail <http://www.python-turbomail.org/>"), |
| | 151 | ('X-TurboMail-Version', version), |
| | 152 | ('X-TurboMail-Extensions', "") |
| | 153 | ] |
| | 154 | |
| | 155 | headers.extend(self.headers) |
| | 156 | |
| | 157 | for header in headers: |
| | 158 | if isinstance(header, (tuple, list)): |
| | 159 | if header[1] is None or ( isinstance(header[1], list) and not header[1] ): continue |
| | 160 | header = list(header) |
| | 161 | header[1] = str(header[1]) |
| | 162 | message.add_header(*header) |
| | 163 | elif isinstance(header, dict): |
| | 164 | message.add_header(**header) |
| | 165 | |
| | 166 | self._mime = message |
| | 167 | self._processed = True |
| | 168 | self._dirty = False |
| | 169 | |
| | 170 | return message |
| | 171 | |
| | 172 | def attach(self, file, name=None): |
| | 173 | """Attach an on-disk file to this message.""" |
| | 174 | |
| | 175 | part = MIMEBase('application', "octet-stream") |
| | 176 | |
| | 177 | if isinstance(file, (str, unicode)): |
| | 178 | fp = open(file, "rb") |
| | 179 | else: |
| | 180 | assert name is not None, "If attaching a file-like object, you must pass a custom filename, as one can not be inferred." |
| | 181 | fp = file |
| | 182 | |
| | 183 | part.set_payload(fp.read()) |
| | 184 | Encoders.encode_base64(part) |
| | 185 | |
| | 186 | part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None])) |
| 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 | | Additinoally, 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 | | message.add_header('Return-Path', self._normalize(self.sender[0])) |
| 299 | | else: |
| 300 | | message.add_header('Return-Path', self._normalize(self.sender)) |
| 301 | | else: |
| 302 | | message.add_header('Return-Path', self._normalize(self.sender)) |
| 303 | | message.add_header('Sender', self._normalize(self.smtpfrom)) |
| 304 | | message.add_header('Return-Path', self._normalize(self.smtpfrom)) |
| 305 | | message.add_header('Old-Return-Path', self._normalize(self.smtpfrom)) |
| 306 | | |
| 307 | | message.add_header('X-Mailer', "TurboMail TurboGears Extension v.%s" % release.version) |
| 308 | | |
| 309 | | if type(self.headers) == type(()): |
| 310 | | for header in self.headers: |
| 311 | | if type(header) in [type(()), type([])]: |
| 312 | | message.add_header(*header) |
| 313 | | elif type(header) == type({}): |
| 314 | | message.add_header(**header) |
| 315 | | |
| 316 | | if type(self.headers) == type({}): |
| 317 | | for name, header in self.headers.iteritems(): |
| 318 | | if type(header) in [type(()), type([])]: |
| 319 | | message.add_header(name, *header) |
| 320 | | elif type(header) == type({}): |
| 321 | | message.add_header(name, **header) |
| 322 | | else: |
| 323 | | message.add_header(name, header) |
| 324 | | |
| 325 | | self._message = message |
| 326 | | self._processed = True |
| 327 | | self._dirty = False |
| 328 | | |
| 329 | | def __setattr__(self, name, value): |
| 330 | | """Set the dirty flag as properties are updated.""" |
| 331 | | |
| 332 | | self.__dict__[name] = value |
| 333 | | if name != '_dirty': self.__dict__['_dirty'] = True |
| 334 | | |
| 335 | | def __call__(self): |
| 336 | | """Produce a valid MIME-encoded message and return valid input |
| 337 | | for the Dispatch class to process. |
| 338 | | |
| 339 | | @return: Returns a tuple containing sender and recipient e-mail |
| 340 | | addresses and the string output of MIMEMultipart. |
| 341 | | @rtype: tuple |
| 342 | | """ |
| 343 | | |
| 344 | | if not self._processed or self._dirty: |
| 345 | | self._process() |
| 346 | | |
| 347 | | recipients = [] |
| 348 | | |
| 349 | | if isinstance(self.recipient, list): |
| 350 | | recipients.extend(self.recipient) |
| 351 | | else: recipients.append(self.recipient) |
| 352 | | |
| 353 | | if isinstance(self.cc, list): |
| 354 | | recipients.extend(self.cc) |
| 355 | | else: recipients.append(self.cc) |
| 356 | | |
| 357 | | if isinstance(self.bcc, list): |
| 358 | | recipients.extend(self.bcc) |
| 359 | | else: recipients.append(self.bcc) |
| 360 | | |
| 361 | | return dict( |
| 362 | | sender=self.sender, |
| 363 | | to=[[self.recipient], self.recipient][isinstance(self.recipient, list)], |
| 364 | | recipients=[i[1] for i in recipients if isinstance(i, tuple)] + [i for i in recipients if not isinstance(i, tuple)], |
| 365 | | subject=self.subject, |
| 366 | | message=self._message.as_string(), |
| 367 | | ) |
| 368 | | |
| 369 | | |
| 370 | | class KIDMessage(Message): |
| 371 | | """A message that accepts a named template with arguments. |
| 372 | | |
| 373 | | Example usage:: |
| 374 | | |
| 375 | | import turbomail |
| 376 | | message = turbomail.KIDMessage( |
| 377 | | "from@host.com", |
| 378 | | "to@host.com", |
| 379 | | "Subject", |
| 380 | | "app.templates.mail", |
| 381 | | dict() |
| 382 | | ) |
| 383 | | |
| 384 | | Do not specify message.plain or message.rich content - the template |
| 385 | | will override what you set. If you wish to hand-produce content, |
| 386 | | use the Message class. |
| 387 | | """ |
| 388 | | |
| 389 | | def __init__(self, sender, recipient, subject, template, variables={}, **kw): |
| 390 | | """Store the additonal template and variable information. |
| 391 | | |
| 392 | | @param template: A dot-path to a valid KID template. |
| 393 | | @type template: string |
| 394 | | |
| 395 | | @param variables: A dictionary containing named variables to |
| 396 | | pass to the template engine. |
| 397 | | @type variables: dict |
| 398 | | """ |
| 399 | | |
| 400 | | log.warn("Use of KIDMessage is deprecated and will be removed in version 2.1.") |
| 401 | | |
| 402 | | self._template = template |
| 403 | | self._variables = dict(sender=sender, recipient=recipient, subject=subject) |
| 404 | | self._variables.update(variables) |
| 405 | | |
| 406 | | super(KIDMessage, self).__init__(sender, recipient, subject, **kw) |
| 407 | | |
| 408 | | def _process(self): |
| 409 | | """Automatically generate the plain and rich text content.""" |
| 410 | | |
| 411 | | #turbogears.view.base._load_engines() |
| 412 | | |
| 413 | | data = dict() |
| 414 | | |
| 415 | | for (i, j) in self._variables.iteritems(): |
| 416 | | if callable(j): data[i] = j() |
| 417 | | else: data[i] = j |
| 418 | | |
| 419 | | self.plain = turbogears.view.engines.get('kid').render(data, format="plain", template=self._template) |
| 420 | | self.rich = turbogears.view.engines.get('kid').render(data, template=self._template) |
| 421 | | |
| 422 | | return super(KIDMessage, self)._process() |
| | 221 | def _callable(self, var): |
| | 222 | if callable(var): |
| | 223 | return var() |
| | 224 | return var |
| | 225 | |
| | 226 | # Note: Whereas the old queue manager accepted a dictionary of values, the new one will accept the actual Message objects. |
| | 227 | # Instead of accessing pack['recipients'], access message.recipients. The MIME encoded message is str(message). |
| | 228 | # message.subject, message.sender, and message.to |