From 70421df8cf2c9babb0bab55645562af31f09e891 Mon Sep 17 00:00:00 2001 From: androiddrew Date: Thu, 25 Jan 2018 14:05:56 -0500 Subject: [PATCH] Renamed Mailer to Mail. Added attribution back to the flask_mail extension. Refactored ascii_attachment check on message. Continued removal of support code for Python2 --- LICENSE | 2 +- README.md | 58 ++++++++++++++++++++++++- apistar_mail/__init__.py | 8 +++- apistar_mail/mail.py | 92 ++++++++++++++++------------------------ setup.cfg | 5 +++ 5 files changed, 107 insertions(+), 58 deletions(-) create mode 100644 setup.cfg diff --git a/LICENSE b/LICENSE index 206be5d..885af88 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) . All rights reserved. +Copyright (c) 2018 Andrew Bednar . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. diff --git a/README.md b/README.md index c6f5c0c..7d52b63 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,59 @@ # apistar-mail +--- + + `apistar-mail` provides a simple interface to set up SMTP with your APIStar application and send messages from your view functions. Please note this work derives largely from the [flask_mail](https://github.com/mattupstate/flask-mail) extension by 'Dan Jacob' but has been modified extensively to remove Python 2 support and be used as an APIStar component. + + +## Installation + +`$ pip install apistar-mail` + +## Usage + +### Setup + +To send mail messages from your view functions you must include the `MAIL` dictionary in your settings and the mail_componet in. + +``` +from apistar import WSGIApp as App +from apistar_mail import mail_component + +settings = { + 'MAIL': { + 'MAIL_SERVER': 'smtp.example.com', + 'MAIL_USERNAME': 'drew@example.com', + 'MAIL_PASSWORD': 'dontcommitthistoversioncontrol', + 'MAIL_PORT': 587, + 'MAIL_USE_TLS': True, + 'MAIL_DEFAULT_SENDER': 'drew@example.com' + } +} + +components = [ + mail_component +] + +app = App( + settings=settings, + routes=routes, + components=components +) + +``` + +### Sending Messages + +To send a message first include the Mail component for injection into your view. Then create an instance of Message, and pass it to your Mail component using `mail.send(msg)` + +``` +from apistar_mail import Mail, Message + +def send_a_message(mail:Mail): + msg = Message('Hello', + sender='drew@example.com' + recipients=[you@example.com]) + mail.send(msg) + return +``` + -The APIStar-Mail extension provides a simple interface to set up SMTP with your APIStar application and send messages from your view functions. \ No newline at end of file diff --git a/apistar_mail/__init__.py b/apistar_mail/__init__.py index c12f34c..aa6c465 100644 --- a/apistar_mail/__init__.py +++ b/apistar_mail/__init__.py @@ -1 +1,7 @@ -__version__ = '0.1.1' \ No newline at end of file +"""Top-level package for apistar-mail""" + +__author__ = """Drew Bednar""" +__email__ = 'drew@androiddrew.com' +__version__ = '0.1.1' + +from .mail import mail_component, Message, Mail \ No newline at end of file diff --git a/apistar_mail/mail.py b/apistar_mail/mail.py index 6acc5cd..d6af08c 100644 --- a/apistar_mail/mail.py +++ b/apistar_mail/mail.py @@ -11,22 +11,14 @@ from email.mime.text import MIMEText from email.header import Header from email.utils import formatdate, formataddr, make_msgid, parseaddr -from apistar import Settings +from apistar import Settings, Component from .exc import MailUnicodeDecodeError, BadHeaderError -# TODO Figure out why this is necessary charset.add_charset('utf-8', charset.SHORTEST, None, 'utf-8') -# TODO ok there is something going here that you may not really grok yet. check def force_text(s, encoding='utf-8', errors='strict', ): - """ - Similar to smart_text, except that lazy instances are resolved to - strings, rather than kept as lazy objects. - - If strings_only is True, don't convert (some) non-string-like objects. - """ if isinstance(s, str): return s @@ -48,25 +40,6 @@ def force_text(s, encoding='utf-8', errors='strict', ): return s - # try: - # if not isinstance(s, string_types): - # if PY3: - # if isinstance(s, bytes): - # s = text_type(s, encoding, errors) - # else: - # s = text_type(s) - # elif hasattr(s, '__unicode__'): - # s = s.__unicode__() - # else: - # s = text_type(bytes(s), encoding, errors) - # else: - # s = s.decode(encoding, errors) - # except UnicodeDecodeError as e: - # if not isinstance(s, Exception): - # raise FlaskMailUnicodeDecodeError(s, *e.args) - - # return s - def sanitize_subject(subject, encoding='utf-8'): try: @@ -119,6 +92,7 @@ class Attachment: :param content_type: file mimetype :param data: the raw file data :param disposition: content-disposition (if any) + :param headers: additional headers. Useful when HTML emails reference attached images """ def __init__(self, filename=None, content_type=None, data=None, @@ -148,6 +122,8 @@ class Message: :param extra_headers: A dictionary of additional headers for the message :param mail_options: A list of ESMTP options to be used in MAIL FROM command :param rcpt_options: A list of ESMTP options to be used in RCPT commands + :param ascii_attachments: A boolean used to force attachment file names to ascii + """ def __init__(self, subject='', @@ -164,7 +140,8 @@ class Message: charset=None, extra_headers=None, mail_options=None, - rcpt_options=None): + rcpt_options=None, + ascii_attachments=False): if isinstance(sender, tuple): sender = "{} <{}>".format(*sender) @@ -185,6 +162,7 @@ class Message: self.mail_options = mail_options or [] self.rcpt_options = rcpt_options or [] self.attachments = attachments or [] + self.ascii_attachments = ascii_attachments @property def send_to(self): @@ -210,7 +188,6 @@ class Message: def _message(self): """Creates the email""" - ascii_attachments = None # current_app.extensions['mail'].ascii_attachments # What does this do? encoding = self.charset or 'utf-8' attachments = self.attachments or [] @@ -258,7 +235,7 @@ class Message: encode_base64(f) filename = attachment.filename - if filename and ascii_attachments: + if filename and self.ascii_attachments: # force filename to ascii filename = unicodedata.normalize('NFKD', filename) filename = filename.encode('ascii', 'ignore').decode('ascii') @@ -266,7 +243,7 @@ class Message: try: filename and filename.encode('ascii') - # TODO Figure out why this is needed. + except UnicodeEncodeError: filename = ('UTF8', '', filename) @@ -279,12 +256,10 @@ class Message: msg.attach(f) - # TODO FIGURE out why this was needed for PY3 in the original package msg.policy = policy.SMTP return msg - # TODO Figure out if this is needed anymore def as_string(self): return self._message().as_string() @@ -320,12 +295,6 @@ class Message: return True return False - def is_bad_headers(self): - from warnings import warn - msg = 'is_bad_headers is deprecated, use the new has_bad_headers method instead.' - warn(DeprecationWarning(msg), stacklevel=1) - return self.has_bad_headers() - def send(self, connection): """Verifies and sends the message.""" @@ -351,6 +320,8 @@ class Message: :param content_type: file mimetype :param data: the raw file data :param disposition: content-disposition (if any) + :param headers: additional headers. Useful when HTML emails reference attached images + """ self.attachments.append( Attachment(filename, content_type, data, disposition, headers)) @@ -359,16 +330,16 @@ class Message: class Connection: """Handles connection to host""" - def __init__(self, mailer): + def __init__(self, mail): """ configure new connection - :param mailer: the application mail manager + :param mail: the application mail manager """ - self.mailer = mailer + self.mail = mail def __enter__(self): - if self.mailer.mail_suppress_send: + if self.mail.mail_suppress_send: self.host = None else: self.host = self.configure_host() @@ -382,17 +353,17 @@ class Connection: self.host.quit() def configure_host(self): - if self.mailer.mail_use_ssl: - host = smtplib.SMTP_SSL(self.mailer.mail_server, self.mailer.mail_port) + if self.mail.mail_use_ssl: + host = smtplib.SMTP_SSL(self.mail.mail_server, self.mail.mail_port) else: - host = smtplib.SMTP(self.mailer.mail_server, self.mailer.mail_port) + host = smtplib.SMTP(self.mail.mail_server, self.mail.mail_port) - host.set_debuglevel(int(self.mailer.mail_debug)) + host.set_debuglevel(int(self.mail.mail_debug)) - if self.mailer.mail_use_tls: + if self.mail.mail_use_tls: host.starttls() - if self.mailer.mail_user and self.mailer.mail_password: - host.login(self.mailer.mail_user, self.mailer.mail_password) + if self.mail.mail_user and self.mail.mail_password: + host.login(self.mail.mail_user, self.mail.mail_password) return host @@ -414,6 +385,9 @@ class Connection: if message.date is None: message.date = time.time() + if not message.ascii_attachments and self.mail.mail_ascii_attachments: + message.ascii_attachments = True + if self.host: self.host.sendmail(sanitize_address(envelope_from or message.sender), list(sanitize_addresses(message.send_to)), @@ -423,7 +397,7 @@ class Connection: self.num_emails += 1 - if self.num_emails == self.mailer.mail_max_emails: + if self.num_emails == self.mail.mail_max_emails: self.num_emails = 0 if self.host: self.host.quit() @@ -438,17 +412,17 @@ class Connection: self.send(Message(*args, **kwargs)) -class Mailer: +class Mail: """Manages email messaging""" def __init__(self, settings: Settings): """ - Configure a new Mailer + Configure a new Mail manager Args: settings: The application settings dictionary """ - mail_config = settings.get('EMAIL') + mail_config = settings.get('MAIL') self.mail_server = mail_config.get('MAIL_SERVER') self.mail_user = mail_config.get('MAIL_USERNAME') self.mail_password = mail_config.get('MAIL_PASSWORD') @@ -475,8 +449,16 @@ class Mailer: message.send(connection) def send_message(self, *args, **kwargs): + """Shortcut for send(msg). + + Takes same arguments as Message constructor. + """ + self.send(Message(*args, **kwargs)) def connect(self): """Opens a connection to the mail host.""" return Connection(self) + + +mail_component = Component(Mail, preload=True) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a49955a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +python-tag = py35 + +[metadata] +license_file = LICENSE \ No newline at end of file