def set_charset(self, charset): """Set the charset of the payload to a given character set. charset can be a Charset instance, a string naming a character set, or None. If it is a string it will be converted to a Charset instance. If charset is None, the charset parameter will be removed from the Content-Type field. Anything else will generate a TypeError. The message will be assumed to be of type text/* encoded with charset.input_charset. It will be converted to charset.output_charset and encoded properly, if needed, when generating the plain text representation of the message. MIME headers (MIME-Version, Content-Type, Content-Transfer-Encoding) will be added as needed. """ if charset is None: self.del_param('charset') self._charset = None return if not isinstance(charset, Charset): charset = Charset(charset) self._charset = charset if 'MIME-Version' not in self: self.add_header('MIME-Version', '1.0') if 'Content-Type' not in self: self.add_header('Content-Type', 'text/plain', charset=charset.get_output_charset()) else: self.set_param('charset', charset.get_output_charset()) if charset != charset.get_output_charset(): self._payload = charset.body_encode(self._payload) if 'Content-Transfer-Encoding' not in self: cte = charset.get_body_encoding() try: cte(self) except TypeError: self._payload = charset.body_encode(self._payload) self.add_header('Content-Transfer-Encoding', cte)
def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None, html=None): """ Create and send a text/plain message Return a tuple of success or error indicator and message. :param subject: subject of email :type subject: unicode :param text: email body text :type text: unicode :param to: recipients :type to: list :param cc: recipients (CC) :type cc: list :param bcc: recipients (BCC) :type bcc: list :param mail_from: override default mail_from :type mail_from: unicode :param html: html email body text :type html: unicode :rtype: tuple :returns: (is_ok, Description of error or OK message) """ import smtplib import socket from email.message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.charset import Charset, QP from email.utils import formatdate, make_msgid cfg = app.cfg if not cfg.mail_enabled: return ( 0, _("Contact administrator: cannot send password recovery e-mail " "because mail configuration is incomplete.")) mail_from = mail_from or cfg.mail_from logging.debug("send mail, from: {0!r}, subj: {1!r}".format( mail_from, subject)) logging.debug("send mail, to: {0!r}".format(to)) if not to and not cc and not bcc: return 1, _("No recipients, nothing to do") subject = subject.encode(CHARSET) # Create a text/plain body using CRLF (see RFC2822) text = text.replace(u'\n', u'\r\n') text = text.encode(CHARSET) # Create a message using CHARSET and quoted printable # encoding, which should be supported better by mail clients. # TODO: check if its really works better for major mail clients text_msg = Message() charset = Charset(CHARSET) charset.header_encoding = QP charset.body_encoding = QP text_msg.set_charset(charset) # work around a bug in python 2.4.3 and above: text_msg.set_payload('=') if text_msg.as_string().endswith('='): text = charset.body_encode(text) text_msg.set_payload(text) if html: msg = MIMEMultipart('alternative') msg.attach(text_msg) html = html.encode(CHARSET) html_msg = MIMEText(html, 'html') html_msg.set_charset(charset) msg.attach(html_msg) else: msg = text_msg address = encodeAddress(mail_from, charset) msg['From'] = address if to: msg['To'] = ','.join(to) if cc: msg['CC'] = ','.join(cc) msg['Date'] = formatdate() msg['Message-ID'] = make_msgid() msg['Subject'] = Header(subject, charset) # See RFC 3834 section 5: msg['Auto-Submitted'] = 'auto-generated' if cfg.mail_sendmail: if bcc: # Set the BCC. This will be stripped later by sendmail. msg['BCC'] = ','.join(bcc) # Set Return-Path so that it isn't set (generally incorrectly) for us. msg['Return-Path'] = address # Send the message if not cfg.mail_sendmail: try: logging.debug( "trying to send mail (smtp) via smtp server '{0}'".format( cfg.mail_smarthost)) host, port = (cfg.mail_smarthost + ':25').split(':')[:2] server = smtplib.SMTP(host, int(port)) try: # server.set_debuglevel(1) if cfg.mail_username is not None and cfg.mail_password is not None: try: # try to do TLS server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() logging.debug( "tls connection to smtp server established") except Exception: logging.debug( "could not establish a tls connection to smtp server, continuing without tls" ) logging.debug( "trying to log in to smtp server using account '{0}'". format(cfg.mail_username)) server.login(cfg.mail_username, cfg.mail_password) server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string()) finally: try: server.quit() except AttributeError: # in case the connection failed, SMTP has no "sock" attribute pass except smtplib.SMTPException as e: logging.exception("smtp mail failed with an exception.") return 0, str(e) except (os.error, socket.error) as e: logging.exception("smtp mail failed with an exception.") return ( 0, _("Connection to mailserver '%(server)s' failed: %(reason)s", server=cfg.mail_smarthost, reason=str(e))) else: try: logging.debug("trying to send mail (sendmail)") sendmailp = os.popen(cfg.mail_sendmail, "w") # msg contains everything we need, so this is a simple write sendmailp.write(msg.as_string()) sendmail_status = sendmailp.close() if sendmail_status: logging.error("sendmail failed with status: {0!s}".format( sendmail_status)) return 0, str(sendmail_status) except Exception: logging.exception("sendmail failed with an exception.") return 0, _("Mail not sent") logging.debug("Mail sent successfully") return 1, _("Mail sent successfully")
def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None, html=None): """ Create and send a text/plain message Return a tuple of success or error indicator and message. :param subject: subject of email :type subject: unicode :param text: email body text :type text: unicode :param to: recipients :type to: list :param cc: recipients (CC) :type cc: list :param bcc: recipients (BCC) :type bcc: list :param mail_from: override default mail_from :type mail_from: unicode :param html: html email body text :type html: unicode :rtype: tuple :returns: (is_ok, Description of error or OK message) """ import smtplib import socket from email.message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.charset import Charset, QP from email.utils import formatdate, make_msgid cfg = app.cfg if not cfg.mail_enabled: return (0, _("Contact administrator: cannot send password recovery e-mail " "because mail configuration is incomplete.")) mail_from = mail_from or cfg.mail_from logging.debug("send mail, from: {0!r}, subj: {1!r}".format(mail_from, subject)) logging.debug("send mail, to: {0!r}".format(to)) if not to and not cc and not bcc: return 1, _("No recipients, nothing to do") subject = subject.encode(CHARSET) # Create a text/plain body using CRLF (see RFC2822) text = text.replace(u'\n', u'\r\n') text = text.encode(CHARSET) # Create a message using CHARSET and quoted printable # encoding, which should be supported better by mail clients. # TODO: check if its really works better for major mail clients text_msg = Message() charset = Charset(CHARSET) charset.header_encoding = QP charset.body_encoding = QP text_msg.set_charset(charset) # work around a bug in python 2.4.3 and above: text_msg.set_payload('=') if text_msg.as_string().endswith('='): text = charset.body_encode(text) text_msg.set_payload(text) if html: msg = MIMEMultipart('alternative') msg.attach(text_msg) html = html.encode(CHARSET) html_msg = MIMEText(html, 'html') html_msg.set_charset(charset) msg.attach(html_msg) else: msg = text_msg address = encodeAddress(mail_from, charset) msg['From'] = address if to: msg['To'] = ','.join(to) if cc: msg['CC'] = ','.join(cc) msg['Date'] = formatdate() msg['Message-ID'] = make_msgid() msg['Subject'] = Header(subject, charset) # See RFC 3834 section 5: msg['Auto-Submitted'] = 'auto-generated' if cfg.mail_sendmail: if bcc: # Set the BCC. This will be stripped later by sendmail. msg['BCC'] = ','.join(bcc) # Set Return-Path so that it isn't set (generally incorrectly) for us. msg['Return-Path'] = address # Send the message if not cfg.mail_sendmail: try: logging.debug("trying to send mail (smtp) via smtp server '{0}'".format(cfg.mail_smarthost)) host, port = (cfg.mail_smarthost + ':25').split(':')[:2] server = smtplib.SMTP(host, int(port)) try: #server.set_debuglevel(1) if cfg.mail_username is not None and cfg.mail_password is not None: try: # try to do TLS server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() logging.debug("tls connection to smtp server established") except: logging.debug("could not establish a tls connection to smtp server, continuing without tls") logging.debug("trying to log in to smtp server using account '{0}'".format(cfg.mail_username)) server.login(cfg.mail_username, cfg.mail_password) server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string()) finally: try: server.quit() except AttributeError: # in case the connection failed, SMTP has no "sock" attribute pass except smtplib.SMTPException as e: logging.exception("smtp mail failed with an exception.") return 0, str(e) except (os.error, socket.error) as e: logging.exception("smtp mail failed with an exception.") return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s", server=cfg.mail_smarthost, reason=str(e) )) else: try: logging.debug("trying to send mail (sendmail)") sendmailp = os.popen(cfg.mail_sendmail, "w") # msg contains everything we need, so this is a simple write sendmailp.write(msg.as_string()) sendmail_status = sendmailp.close() if sendmail_status: logging.error("sendmail failed with status: {0!s}".format(sendmail_status)) return 0, str(sendmail_status) except: logging.exception("sendmail failed with an exception.") return 0, _("Mail not sent") logging.debug("Mail sent successfully") return 1, _("Mail sent successfully")
class UnicodeMessage(object): ''' Wrapper around a email.message.Message, that allows to interact with the message using decoded unicode strings. Part of the interface to Message is supported. The interface methods return normal unicode strings, with the email-specific encoding parts removed. The underlying message might be transformed by this class and should not be used elsewhere. ''' def __init__(self, msg): ''' Create a message that is fully utf-8 encoded. msg is the original message. ''' if not isinstance(msg, email.message.Message): raise TypeError('msg is not a Message') self._msg = msg charset = msg.get_content_charset() or 'utf-8' self._body_charset = Charset(input_charset=charset) assert self._body_charset.header_encoding in [None, QP] assert self._body_charset.body_encoding in [None, QP] if not self._msg.has_key('Subject'): self._msg.add_header('Subject', '') def __str__(self): return self.as_string() @property def id(self): return self['Message-Id'] def as_string(self): """ Returns the message as a string encoded with utf-8, avoiding the escaping of 'From' lines. """ io = StringIO() g = Generator(io, False) # second argument means "should I mangle From?" g.flatten(self._msg) return io.getvalue() # Delegate to Message def __getitem__(self, name): '''Get a header value, from the message, decoded and as a unicode string. If the header does not exist, None is returned''' value = self._msg[name] if value is None: return None return u''.join(to_unicode(*tupl) for tupl in decode_header(value)) def replace_header(self, name, value): '''Forwards the call to replace_header. name the id of the header. If it does not exist yet, it is newly created. This behavior is different from the standard message. value is passed as a unicode string. This method tries to avoid encoding the value with a Header (i.e when the value is an ascii string). ''' assert isinstance(value, unicode) try: header = value.encode('ascii') except UnicodeEncodeError: header = Header(value.encode('utf-8'), 'UTF-8').encode() if self._msg.has_key(name): self._msg.replace_header(name, header) else: self._msg.add_header(name, header) def get_payload(self, i=None, decode=False): ''' Forwards the call to get_payload. Instances of the type email.message.Message are wrapped as a UnicodeMessage. Strings are returned as unicode. ''' payload = self._msg.get_payload(i, decode) if isinstance(payload, list): return [UnicodeMessage(msg) for msg in payload] elif isinstance(payload, email.message.Message): return UnicodeMessage(payload) elif isinstance(payload, str): return to_unicode(payload, self._msg.get_content_charset()) return payload def get_clean_payload(self, forbidden_words): ''' Gets a text payload, with the given forbidden words replaced. forbidden_words a dictionary containing pairs of (word_to_replace, replacement). ''' assert isinstance(forbidden_words, dict) payload = self.get_payload(decode=True) assert isinstance(payload, unicode) payload = payload.split('\n') return '\n'.join( ' '.join(self._clean_word(word, forbidden_words) for word in line.split(' ')) for line in payload) def _clean_word(self, word, forbidden_words): ''' Returns a replacement if the given word is in the forbidden words dictionary. Otherwise, the word is returned unchanged. The word is striped of punctuation (i.e. period, asterisks) and converted to lower for the comparison. ''' punctuation = '.!?*()\'"[]-_+=:;<>,/' match = word.lower().strip(punctuation) if match in forbidden_words: replacement = forbidden_words[match] word = re.sub(match, replacement, word, flags=re.IGNORECASE) return word def set_payload(self, payload): ''' Forwards the call to set_payload. If the payload is text, it is passed as a unicode string. Text is encoded again before being passed. The content encoding is changed to quoted printable to avoid encoding incompatibilities. ''' assert not isinstance(payload, str) if isinstance(payload, unicode): self.replace_header('Content-Transfer-Encoding', u'quoted-printable') payload = self._body_charset.body_encode( payload.encode(self._body_charset.input_charset), convert=False) self._msg.set_payload(payload) from email.Iterators import walk def __getattr__(self, name): return getattr(self._msg, name)
def sendmail(request, to, subject, text, mail_from=None): """ Create and send a text/plain message Return a tuple of success or error indicator and message. @param request: the request object @param to: recipients (list) @param subject: subject of email (unicode) @param text: email body text (unicode) @param mail_from: override default mail_from @type mail_from: unicode @rtype: tuple @return: (is_ok, Description of error or OK message) """ import smtplib, socket from email.message import Message from email.charset import Charset, QP from email.utils import formatdate, make_msgid _ = request.getText cfg = request.cfg mail_from = mail_from or cfg.mail_from logging.debug("send mail, from: %r, subj: %r" % (mail_from, subject)) logging.debug("send mail, to: %r" % (to, )) if not to: return (1, _("No recipients, nothing to do")) subject = subject.encode(config.charset) # Create a text/plain body using CRLF (see RFC2822) text = text.replace(u'\n', u'\r\n') text = text.encode(config.charset) # Create a message using config.charset and quoted printable # encoding, which should be supported better by mail clients. # TODO: check if its really works better for major mail clients msg = Message() charset = Charset(config.charset) charset.header_encoding = QP charset.body_encoding = QP msg.set_charset(charset) # work around a bug in python 2.4.3 and above: msg.set_payload('=') if msg.as_string().endswith('='): text = charset.body_encode(text) msg.set_payload(text) # Create message headers # Don't expose emails addreses of the other subscribers, instead we # use the same mail_from, e.g. u"Jürgen Wiki <*****@*****.**>" address = encodeAddress(mail_from, charset) msg['From'] = address msg['To'] = address msg['Date'] = formatdate() msg['Message-ID'] = make_msgid() msg['Subject'] = Header(subject, charset) # See RFC 3834 section 5: msg['Auto-Submitted'] = 'auto-generated' if cfg.mail_sendmail: # Set the BCC. This will be stripped later by sendmail. msg['BCC'] = ','.join(to) # Set Return-Path so that it isn't set (generally incorrectly) for us. msg['Return-Path'] = address # Send the message if not cfg.mail_sendmail: try: logging.debug("trying to send mail (smtp) via smtp server '%s'" % cfg.mail_smarthost) host, port = (cfg.mail_smarthost + ':25').split(':')[:2] server = smtplib.SMTP(host, int(port)) try: #server.set_debuglevel(1) if cfg.mail_login: user, pwd = cfg.mail_login.split() try: # try to do tls server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() logging.debug( "tls connection to smtp server established") except: logging.debug( "could not establish a tls connection to smtp server, continuing without tls" ) logging.debug( "trying to log in to smtp server using account '%s'" % user) server.login(user, pwd) server.sendmail(mail_from, to, msg.as_string()) finally: try: server.quit() except AttributeError: # in case the connection failed, SMTP has no "sock" attribute pass except UnicodeError, e: logging.exception("unicode error [%r -> %r]" % ( mail_from, to, )) return (0, str(e)) except smtplib.SMTPException, e: logging.exception("smtp mail failed with an exception.") return (0, str(e))
class UnicodeMessage(object): ''' Wrapper around a email.message.Message, that allows to interact with the message using decoded unicode strings. Part of the interface to Message is supported. The interface methods return normal unicode strings, with the email-specific encoding parts removed. The underlying message might be transformed by this class and should not be used elsewhere. ''' def __init__(self, msg): ''' Create a message that is fully utf-8 encoded. msg is the original message. ''' if not isinstance(msg, email.message.Message): raise TypeError('msg is not a Message') self._msg = msg charset = msg.get_content_charset() or 'utf-8' self._body_charset = Charset(input_charset=charset) assert self._body_charset.header_encoding in [None, QP] assert self._body_charset.body_encoding in [None, QP] if not self._msg.has_key('Subject'): self._msg.add_header('Subject', '') def __str__(self): return self.as_string() @property def id(self): return self['Message-Id'] def as_string(self): """ Returns the message as a string encoded with utf-8, avoiding the escaping of 'From' lines. """ io = StringIO() g = Generator(io, False) # second argument means "should I mangle From?" g.flatten(self._msg) return io.getvalue() # Delegate to Message def __getitem__(self, name): '''Get a header value, from the message, decoded and as a unicode string. If the header does not exist, None is returned''' value = self._msg[name] if value is None: return None return u''.join(to_unicode(*tupl) for tupl in decode_header(value)) def replace_header(self, name, value): '''Forwards the call to replace_header. name the id of the header. If it does not exist yet, it is newly created. This behavior is different from the standard message. value is passed as a unicode string. This method tries to avoid encoding the value with a Header (i.e when the value is an ascii string). ''' assert isinstance(value, unicode) try: header = value.encode('ascii') except UnicodeEncodeError: header = Header(value.encode('utf-8'), 'UTF-8').encode() if self._msg.has_key(name): self._msg.replace_header(name, header) else: self._msg.add_header(name, header) def get_payload(self, i=None, decode=False): ''' Forwards the call to get_payload. Instances of the type email.message.Message are wrapped as a UnicodeMessage. Strings are returned as unicode. ''' payload = self._msg.get_payload(i, decode) if isinstance(payload, list): return [UnicodeMessage(msg) for msg in payload] elif isinstance(payload, email.message.Message): return UnicodeMessage(payload) elif isinstance(payload, str): return to_unicode(payload, self._msg.get_content_charset()) return payload def get_clean_payload(self, forbidden_words): ''' Gets a text payload, with the given forbidden words replaced. forbidden_words a dictionary containing pairs of (word_to_replace, replacement). ''' assert isinstance(forbidden_words, dict) payload = self.get_payload(decode=True) assert isinstance(payload, unicode) payload = payload.split('\n') return '\n'.join(' '.join( self._clean_word(word, forbidden_words) for word in line.split(' ')) for line in payload) def _clean_word(self, word, forbidden_words): ''' Returns a replacement if the given word is in the forbidden words dictionary. Otherwise, the word is returned unchanged. The word is striped of punctuation (i.e. period, asterisks) and converted to lower for the comparison. ''' punctuation = '.!?*()\'"[]-_+=:;<>,/' match = word.lower().strip(punctuation) if match in forbidden_words: replacement = forbidden_words[match] word = re.sub(match, replacement, word, flags=re.IGNORECASE) return word def set_payload(self, payload): ''' Forwards the call to set_payload. If the payload is text, it is passed as a unicode string. Text is encoded again before being passed. The content encoding is changed to quoted printable to avoid encoding incompatibilities. ''' assert not isinstance(payload, str) if isinstance(payload, unicode): self.replace_header('Content-Transfer-Encoding', u'quoted-printable') payload = self._body_charset.body_encode(payload.encode( self._body_charset.input_charset), convert=False) self._msg.set_payload(payload) from email.Iterators import walk def __getattr__(self, name): return getattr(self._msg, name)