########################################################################## # # Copyright (c) 2005 Imaginary Landscape LLC and Contributors. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ########################################################################## __all__ = ['EmailTemplate', 'send_mail'] from templatetools import FilePageTemplate import cgi import re from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart import smtplib import htmlrender class EmailTemplate(FilePageTemplate): """ A system for templates that are intended to be sent as emails. Templates can contain markup like:
subject
and so on, to define headers. When rendering a template, an email object will be returned (from the email module in the standard library). Templates will be rendered twice, once for HTML and once for a text version. The template may test for the variable "email_html" and "html_text" to see which version is being created. The text version will be converted from HTML to text via the htmlrender module. The send_mail() module will load, render, and send the email in one step. """ def __call__(self, context=None, *args, **kw): if context is None: context = {} else: context = context.copy() include_html = popdefault(kw, 'include_html', True) include_text = popdefault(kw, 'include_text', True) assert include_html or include_text, ( "You must have at least one of include_html or " "include_text as True") html = text = None if include_html: context['email_html'] = True context['email_text'] = False html = FilePageTemplate.__call__( self, context=context, *args, **kw) if include_text: context['email_html'] = False context['email_text'] = True text = FilePageTemplate.__call__( self, context=context, *args, **kw) if html: headers, html = self.parse_headers(html) if text: headers, text = self.parse_headers(text) text = htmlrender.render(text) return self.create_message(headers, html, text) def create_message(self, headers, html, text): msg = MIMEMultipart() for name, value in headers: msg[name] = value msg.set_type('multipart/alternative') msg.preamble = '' msg.epilogue = '' if text: text_msg = MIMEText(text) text_msg.set_type('text/plain') text_msg.set_param('charset', 'UTF-8') msg.attach(text_msg) if html: html_msg = MIMEText(html) html_msg.set_type('text/html') html_msg.set_param('charset', 'UTF-8') msg.attach(html_msg) return msg header_start_re = re.compile(r'
') header_end_re = re.compile(r'
') def parse_headers(self, markup): """ Looks for
...
in an HTML file, and return (header_list, rest_of_body). header_list is a list of (name, content) tuples. """ headers = [] while 1: match_start = self.header_start_re.search(markup) if not match_start: break header_name = match_start.group(1) rest = markup[match_start.end():] match_end = self.header_end_re.search(rest) assert match_end, ( "Bad markup, no found: %r" % rest) content = rest[:match_end.start()] content = self.normalize_whitespace(content) headers.append((header_name.strip(), content)) markup = (markup[:match_start.start()] + markup[match_end.end()+match_start.end():]) return headers, markup.strip() whitespace_re = re.compile(r'[ \t\n\r]+') def normalize_whitespace(self, s): s = s.strip() s = self.whitespace_re.sub(' ', s) return s def popdefault(d, key, default=None): """ Pop key from dictionary d (and return), or return default """ if d.has_key(key): v = d[key] del d[key] return v else: return default def send_mail(template, to_address, from_address, **options): """ Send mail to to_address (a single email, or a list of emails), from from_address, using the given template. If you give a string, it will be considered a filename and the template will be loaded from that location. All options (plus to_address and from_address) will be passed into the template as options. Any options that start with 'header_' will also be used as headers (though the template can override these, and takes precedence). If you don't want to send HTML mail, use include_html=False; include_text=False to suppress the text version (though there's little purpose to doing that). The mail will be sent out via SMTP on the localhost. """ if isinstance(template, str): # @@: it's a shame `here` is undefined for this case: template = EmailTemplate(template, None) msg = template(to_address=to_address, from_address=from_address, **options) if isinstance(to_address, (str, unicode)): to_address = [to_address] for single_to_address in to_address: msg['To'] = single_to_address msg['From'] = from_address for name, value in options.items(): if name.startswith('header_'): name = name[len('header_'):] if not msg.get(name): msg[name] = value smtp_server = options.get('smtp_server', 'localhost') server = smtplib.SMTP(smtp_server) server.sendmail(from_address, to_address, str(msg)) server.quit() return msg