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

master
androiddrew 7 years ago
parent bb00097b0a
commit 70421df8cf

@ -1,4 +1,4 @@
Copyright (c) <year> <owner> . 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: 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. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

@ -1,3 +1,59 @@
# apistar-mail # 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.

@ -1 +1,7 @@
__version__ = '0.1.1' """Top-level package for apistar-mail"""
__author__ = """Drew Bednar"""
__email__ = 'drew@androiddrew.com'
__version__ = '0.1.1'
from .mail import mail_component, Message, Mail

@ -11,22 +11,14 @@ from email.mime.text import MIMEText
from email.header import Header from email.header import Header
from email.utils import formatdate, formataddr, make_msgid, parseaddr from email.utils import formatdate, formataddr, make_msgid, parseaddr
from apistar import Settings from apistar import Settings, Component
from .exc import MailUnicodeDecodeError, BadHeaderError from .exc import MailUnicodeDecodeError, BadHeaderError
# TODO Figure out why this is necessary
charset.add_charset('utf-8', charset.SHORTEST, None, 'utf-8') 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', ): 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): if isinstance(s, str):
return s return s
@ -48,25 +40,6 @@ def force_text(s, encoding='utf-8', errors='strict', ):
return s 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'): def sanitize_subject(subject, encoding='utf-8'):
try: try:
@ -119,6 +92,7 @@ class Attachment:
:param content_type: file mimetype :param content_type: file mimetype
:param data: the raw file data :param data: the raw file data
:param disposition: content-disposition (if any) :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, 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 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 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 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='', def __init__(self, subject='',
@ -164,7 +140,8 @@ class Message:
charset=None, charset=None,
extra_headers=None, extra_headers=None,
mail_options=None, mail_options=None,
rcpt_options=None): rcpt_options=None,
ascii_attachments=False):
if isinstance(sender, tuple): if isinstance(sender, tuple):
sender = "{} <{}>".format(*sender) sender = "{} <{}>".format(*sender)
@ -185,6 +162,7 @@ class Message:
self.mail_options = mail_options or [] self.mail_options = mail_options or []
self.rcpt_options = rcpt_options or [] self.rcpt_options = rcpt_options or []
self.attachments = attachments or [] self.attachments = attachments or []
self.ascii_attachments = ascii_attachments
@property @property
def send_to(self): def send_to(self):
@ -210,7 +188,6 @@ class Message:
def _message(self): def _message(self):
"""Creates the email""" """Creates the email"""
ascii_attachments = None # current_app.extensions['mail'].ascii_attachments # What does this do?
encoding = self.charset or 'utf-8' encoding = self.charset or 'utf-8'
attachments = self.attachments or [] attachments = self.attachments or []
@ -258,7 +235,7 @@ class Message:
encode_base64(f) encode_base64(f)
filename = attachment.filename filename = attachment.filename
if filename and ascii_attachments: if filename and self.ascii_attachments:
# force filename to ascii # force filename to ascii
filename = unicodedata.normalize('NFKD', filename) filename = unicodedata.normalize('NFKD', filename)
filename = filename.encode('ascii', 'ignore').decode('ascii') filename = filename.encode('ascii', 'ignore').decode('ascii')
@ -266,7 +243,7 @@ class Message:
try: try:
filename and filename.encode('ascii') filename and filename.encode('ascii')
# TODO Figure out why this is needed.
except UnicodeEncodeError: except UnicodeEncodeError:
filename = ('UTF8', '', filename) filename = ('UTF8', '', filename)
@ -279,12 +256,10 @@ class Message:
msg.attach(f) msg.attach(f)
# TODO FIGURE out why this was needed for PY3 in the original package
msg.policy = policy.SMTP msg.policy = policy.SMTP
return msg return msg
# TODO Figure out if this is needed anymore
def as_string(self): def as_string(self):
return self._message().as_string() return self._message().as_string()
@ -320,12 +295,6 @@ class Message:
return True return True
return False 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): def send(self, connection):
"""Verifies and sends the message.""" """Verifies and sends the message."""
@ -351,6 +320,8 @@ class Message:
:param content_type: file mimetype :param content_type: file mimetype
:param data: the raw file data :param data: the raw file data
:param disposition: content-disposition (if any) :param disposition: content-disposition (if any)
:param headers: additional headers. Useful when HTML emails reference attached images
""" """
self.attachments.append( self.attachments.append(
Attachment(filename, content_type, data, disposition, headers)) Attachment(filename, content_type, data, disposition, headers))
@ -359,16 +330,16 @@ class Message:
class Connection: class Connection:
"""Handles connection to host""" """Handles connection to host"""
def __init__(self, mailer): def __init__(self, mail):
""" """
configure new connection configure new connection
:param mailer: the application mail manager :param mail: the application mail manager
""" """
self.mailer = mailer self.mail = mail
def __enter__(self): def __enter__(self):
if self.mailer.mail_suppress_send: if self.mail.mail_suppress_send:
self.host = None self.host = None
else: else:
self.host = self.configure_host() self.host = self.configure_host()
@ -382,17 +353,17 @@ class Connection:
self.host.quit() self.host.quit()
def configure_host(self): def configure_host(self):
if self.mailer.mail_use_ssl: if self.mail.mail_use_ssl:
host = smtplib.SMTP_SSL(self.mailer.mail_server, self.mailer.mail_port) host = smtplib.SMTP_SSL(self.mail.mail_server, self.mail.mail_port)
else: 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() host.starttls()
if self.mailer.mail_user and self.mailer.mail_password: if self.mail.mail_user and self.mail.mail_password:
host.login(self.mailer.mail_user, self.mailer.mail_password) host.login(self.mail.mail_user, self.mail.mail_password)
return host return host
@ -414,6 +385,9 @@ class Connection:
if message.date is None: if message.date is None:
message.date = time.time() message.date = time.time()
if not message.ascii_attachments and self.mail.mail_ascii_attachments:
message.ascii_attachments = True
if self.host: if self.host:
self.host.sendmail(sanitize_address(envelope_from or message.sender), self.host.sendmail(sanitize_address(envelope_from or message.sender),
list(sanitize_addresses(message.send_to)), list(sanitize_addresses(message.send_to)),
@ -423,7 +397,7 @@ class Connection:
self.num_emails += 1 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 self.num_emails = 0
if self.host: if self.host:
self.host.quit() self.host.quit()
@ -438,17 +412,17 @@ class Connection:
self.send(Message(*args, **kwargs)) self.send(Message(*args, **kwargs))
class Mailer: class Mail:
"""Manages email messaging""" """Manages email messaging"""
def __init__(self, settings: Settings): def __init__(self, settings: Settings):
""" """
Configure a new Mailer Configure a new Mail manager
Args: Args:
settings: The application settings dictionary 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_server = mail_config.get('MAIL_SERVER')
self.mail_user = mail_config.get('MAIL_USERNAME') self.mail_user = mail_config.get('MAIL_USERNAME')
self.mail_password = mail_config.get('MAIL_PASSWORD') self.mail_password = mail_config.get('MAIL_PASSWORD')
@ -475,8 +449,16 @@ class Mailer:
message.send(connection) message.send(connection)
def send_message(self, *args, **kwargs): def send_message(self, *args, **kwargs):
"""Shortcut for send(msg).
Takes same arguments as Message constructor.
"""
self.send(Message(*args, **kwargs)) self.send(Message(*args, **kwargs))
def connect(self): def connect(self):
"""Opens a connection to the mail host.""" """Opens a connection to the mail host."""
return Connection(self) return Connection(self)
mail_component = Component(Mail, preload=True)

@ -0,0 +1,5 @@
[bdist_wheel]
python-tag = py35
[metadata]
license_file = LICENSE
Loading…
Cancel
Save