From 46376b8f1281c084bf32c34f2ba432686992ba71 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 16:19:44 +0000 Subject: [PATCH] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHANGES | 2 - README.rst | 2 +- docs/changelog.rst | 2 +- docs/conf.py | 108 ++++--- flask_mail.py | 284 ++++++++++-------- scripts/release.py | 114 ++++---- setup.cfg | 2 +- setup.py | 48 ++- tests.py | 708 +++++++++++++++++++++++++-------------------- 9 files changed, 684 insertions(+), 586 deletions(-) diff --git a/CHANGES b/CHANGES index 687360e..df25855 100644 --- a/CHANGES +++ b/CHANGES @@ -105,5 +105,3 @@ Version 0.7 and prior --------------------- Initial development by Dan Jacob and Ron DuPlain. Previously there was not a change log. - - diff --git a/README.rst b/README.rst index 80b1c9b..6e515aa 100644 --- a/README.rst +++ b/README.rst @@ -5,4 +5,4 @@ Flask-Mail Flask-Mail is a Flask extension providing simple email sending capabilities. -Documentation: http://packages.python.org/Flask-Mail \ No newline at end of file +Documentation: http://packages.python.org/Flask-Mail diff --git a/docs/changelog.rst b/docs/changelog.rst index 8a05a51..d6c5f48 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1 @@ -.. include:: ../CHANGES \ No newline at end of file +.. include:: ../CHANGES diff --git a/docs/conf.py b/docs/conf.py index d769454..c1f0476 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # flask-mail documentation build configuration file, created by # sphinx-quickstart on Fri May 28 11:39:14 2010. @@ -11,190 +10,187 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('_themes')) +sys.path.insert(0, os.path.abspath("..")) +sys.path.append(os.path.abspath("_themes")) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Flask-Mail' -copyright = u'2010, Dan Jacob' +project = "Flask-Mail" +copyright = "2010, Dan Jacob" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.9.1' +version = "0.9.1" # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['_build'] +exclude_trees = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -#pygments_style = 'sphinx' +# pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'flask_small' -#html_theme = 'default' -html_theme_options = { - 'index_logo': 'flask-mail.png', - 'github_fork': None -} +html_theme = "flask_small" +# html_theme = 'default' +html_theme_options = {"index_logo": "flask-mail.png", "github_fork": None} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'flask-maildoc' +htmlhelp_basename = "flask-maildoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'flask-mail.tex', u'flask-mail Documentation', - u'Dan Jacob', 'manual'), + ("index", "flask-mail.tex", "flask-mail Documentation", "Dan Jacob", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True diff --git a/flask_mail.py b/flask_mail.py index cf8119a..6abc60f 100644 --- a/flask_mail.py +++ b/flask_mail.py @@ -1,34 +1,33 @@ -# -*- coding: utf-8 -*- """ - flaskext.mail - ~~~~~~~~~~~~~ +flaskext.mail +~~~~~~~~~~~~~ - Flask extension for sending email. +Flask extension for sending email. - :copyright: (c) 2010 by Dan Jacob. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2010 by Dan Jacob. +:license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - -__version__ = '0.9.1' +__version__ = "0.9.1" import re -import blinker import smtplib import sys import time import unicodedata - +from contextlib import contextmanager from email import charset from email.encoders import encode_base64 +from email.header import Header from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.header import Header -from email.utils import formatdate, formataddr, make_msgid, parseaddr -from contextlib import contextmanager +from email.utils import formataddr +from email.utils import formatdate +from email.utils import make_msgid +from email.utils import parseaddr +import blinker from flask import current_app PY3 = sys.version_info[0] == 3 @@ -36,16 +35,17 @@ PY34 = PY3 and sys.version_info[1] >= 4 if PY3: - string_types = str, + string_types = (str,) text_type = str from email import policy + message_policy = policy.SMTP else: - string_types = basestring, + string_types = (basestring,) text_type = unicode message_policy = None -charset.add_charset('utf-8', charset.SHORTEST, None, 'utf-8') +charset.add_charset("utf-8", charset.SHORTEST, None, "utf-8") class FlaskMailUnicodeDecodeError(UnicodeDecodeError): @@ -55,10 +55,10 @@ def __init__(self, obj, *args): def __str__(self): original = UnicodeDecodeError.__str__(self) - return '%s. You passed in %r (%s)' % (original, self.obj, type(self.obj)) + return "%s. You passed in %r (%s)" % (original, self.obj, type(self.obj)) -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. @@ -75,7 +75,7 @@ def force_text(s, encoding='utf-8', errors='strict'): s = text_type(s, encoding, errors) else: s = text_type(s) - elif hasattr(s, '__unicode__'): + elif hasattr(s, "__unicode__"): s = s.__unicode__() else: s = text_type(bytes(s), encoding, errors) @@ -85,21 +85,22 @@ def force_text(s, encoding='utf-8', errors='strict'): if not isinstance(s, Exception): raise FlaskMailUnicodeDecodeError(s, *e.args) else: - s = ' '.join([force_text(arg, encoding, strings_only, - errors) for arg in s]) + s = " ".join([force_text(arg, encoding, strings_only, errors) for arg in s]) return s -def sanitize_subject(subject, encoding='utf-8'): + +def sanitize_subject(subject, encoding="utf-8"): try: - subject.encode('ascii') + subject.encode("ascii") except UnicodeEncodeError: try: subject = Header(subject, encoding).encode() except UnicodeEncodeError: - subject = Header(subject, 'utf-8').encode() + subject = Header(subject, "utf-8").encode() return subject -def sanitize_address(addr, encoding='utf-8'): + +def sanitize_address(addr, encoding="utf-8"): if isinstance(addr, string_types): addr = parseaddr(force_text(addr)) nm, addr = addr @@ -107,31 +108,32 @@ def sanitize_address(addr, encoding='utf-8'): try: nm = Header(nm, encoding).encode() except UnicodeEncodeError: - nm = Header(nm, 'utf-8').encode() + nm = Header(nm, "utf-8").encode() try: - addr.encode('ascii') + addr.encode("ascii") except UnicodeEncodeError: # IDN - if '@' in addr: - localpart, domain = addr.split('@', 1) + if "@" in addr: + localpart, domain = addr.split("@", 1) localpart = str(Header(localpart, encoding)) - domain = domain.encode('idna').decode('ascii') - addr = '@'.join([localpart, domain]) + domain = domain.encode("idna").decode("ascii") + addr = "@".join([localpart, domain]) else: addr = Header(addr, encoding).encode() return formataddr((nm, addr)) -def sanitize_addresses(addresses, encoding='utf-8'): +def sanitize_addresses(addresses, encoding="utf-8"): return map(lambda e: sanitize_address(e, encoding), addresses) def _has_newline(line): """Used by has_bad_header to check for \\r or \\n""" - if line and ('\r' in line or '\n' in line): + if line and ("\r" in line or "\n" in line): return True return False -class Connection(object): + +class Connection: """Handles connection to host.""" def __init__(self, mail): @@ -175,8 +177,9 @@ def send(self, message, envelope_from=None): assert message.send_to, "No recipients have been added" assert message.sender, ( - "The message does not specify a sender and a default sender " - "has not been configured") + "The message does not specify a sender and a default sender " + "has not been configured" + ) if message.has_bad_headers(): raise BadHeaderError @@ -185,11 +188,13 @@ def send(self, message, envelope_from=None): message.date = time.time() if self.host: - self.host.sendmail(sanitize_address(envelope_from or message.sender), - list(sanitize_addresses(message.send_to)), - message.as_bytes() if PY3 else message.as_string(), - message.mail_options, - message.rcpt_options) + self.host.sendmail( + sanitize_address(envelope_from or message.sender), + list(sanitize_addresses(message.send_to)), + message.as_bytes() if PY3 else message.as_string(), + message.mail_options, + message.rcpt_options, + ) email_dispatched.send(message, app=current_app._get_current_object()) @@ -216,7 +221,7 @@ class BadHeaderError(Exception): pass -class Attachment(object): +class Attachment: """Encapsulates file attachment information. :versionadded: 0.3.5 @@ -227,16 +232,22 @@ class Attachment(object): :param disposition: content-disposition (if any) """ - def __init__(self, filename=None, content_type=None, data=None, - disposition=None, headers=None): + def __init__( + self, + filename=None, + content_type=None, + data=None, + disposition=None, + headers=None, + ): self.filename = filename self.content_type = content_type self.data = data - self.disposition = disposition or 'attachment' + self.disposition = disposition or "attachment" self.headers = headers or {} -class Message(object): +class Message: """Encapsulates an email message. :param subject: email subject header @@ -256,23 +267,25 @@ class Message(object): :param rcpt_options: A list of ESMTP options to be used in RCPT commands """ - def __init__(self, subject='', - recipients=None, - body=None, - html=None, - alts=None, - sender=None, - cc=None, - bcc=None, - attachments=None, - reply_to=None, - date=None, - charset=None, - extra_headers=None, - mail_options=None, - rcpt_options=None): - - sender = sender or current_app.extensions['mail'].default_sender + def __init__( + self, + subject="", + recipients=None, + body=None, + html=None, + alts=None, + sender=None, + cc=None, + bcc=None, + attachments=None, + reply_to=None, + date=None, + charset=None, + extra_headers=None, + mail_options=None, + rcpt_options=None, + ): + sender = sender or current_app.extensions["mail"].default_sender if isinstance(sender, tuple): sender = "%s <%s>" % sender @@ -300,26 +313,26 @@ def send_to(self): @property def html(self): - return self.alts.get('html') + return self.alts.get("html") @html.setter def html(self, value): if value is None: - self.alts.pop('html', None) + self.alts.pop("html", None) else: - self.alts['html'] = value + self.alts["html"] = value - def _mimetext(self, text, subtype='plain'): + def _mimetext(self, text, subtype="plain"): """Creates a MIMEText object with the given subtype (default: 'plain') If the text is unicode, the utf-8 charset is used. """ - charset = self.charset or 'utf-8' + charset = self.charset or "utf-8" return MIMEText(text, _subtype=subtype, _charset=charset) def _message(self): """Creates the email""" - ascii_attachments = current_app.extensions['mail'].ascii_attachments - encoding = self.charset or 'utf-8' + ascii_attachments = current_app.extensions["mail"].ascii_attachments + encoding = self.charset or "utf-8" attachments = self.attachments or [] @@ -333,55 +346,55 @@ def _message(self): else: # Anything else msg = MIMEMultipart() - alternative = MIMEMultipart('alternative') - alternative.attach(self._mimetext(self.body, 'plain')) + alternative = MIMEMultipart("alternative") + alternative.attach(self._mimetext(self.body, "plain")) for mimetype, content in self.alts.items(): alternative.attach(self._mimetext(content, mimetype)) msg.attach(alternative) if self.subject: - msg['Subject'] = sanitize_subject(force_text(self.subject), encoding) + msg["Subject"] = sanitize_subject(force_text(self.subject), encoding) - msg['From'] = sanitize_address(self.sender, encoding) - msg['To'] = ', '.join(list(set(sanitize_addresses(self.recipients, encoding)))) + msg["From"] = sanitize_address(self.sender, encoding) + msg["To"] = ", ".join(list(set(sanitize_addresses(self.recipients, encoding)))) - msg['Date'] = formatdate(self.date, localtime=True) + msg["Date"] = formatdate(self.date, localtime=True) # see RFC 5322 section 3.6.4. - msg['Message-ID'] = self.msgId + msg["Message-ID"] = self.msgId if self.cc: - msg['Cc'] = ', '.join(list(set(sanitize_addresses(self.cc, encoding)))) + msg["Cc"] = ", ".join(list(set(sanitize_addresses(self.cc, encoding)))) if self.reply_to: - msg['Reply-To'] = sanitize_address(self.reply_to, encoding) + msg["Reply-To"] = sanitize_address(self.reply_to, encoding) if self.extra_headers: for k, v in self.extra_headers.items(): msg[k] = v - SPACES = re.compile(r'[\s]+', re.UNICODE) + SPACES = re.compile(r"[\s]+", re.UNICODE) for attachment in attachments: - f = MIMEBase(*attachment.content_type.split('/')) + f = MIMEBase(*attachment.content_type.split("/")) f.set_payload(attachment.data) encode_base64(f) filename = attachment.filename if filename and ascii_attachments: # force filename to ascii - filename = unicodedata.normalize('NFKD', filename) - filename = filename.encode('ascii', 'ignore').decode('ascii') - filename = SPACES.sub(u' ', filename).strip() + filename = unicodedata.normalize("NFKD", filename) + filename = filename.encode("ascii", "ignore").decode("ascii") + filename = SPACES.sub(" ", filename).strip() try: - filename and filename.encode('ascii') + filename and filename.encode("ascii") except UnicodeEncodeError: if not PY3: - filename = filename.encode('utf8') - filename = ('UTF8', '', filename) + filename = filename.encode("utf8") + filename = ("UTF8", "", filename) - f.add_header('Content-Disposition', - attachment.disposition, - filename=filename) + f.add_header( + "Content-Disposition", attachment.disposition, filename=filename + ) for key, value in attachment.headers.items(): f.add_header(key, value) @@ -398,8 +411,8 @@ def as_string(self): def as_bytes(self): if PY34: return self._message().as_bytes() - else: # fallback for old Python (3) versions - return self._message().as_string().encode(self.charset or 'utf-8') + else: # fallback for old Python (3) versions + return self._message().as_string().encode(self.charset or "utf-8") def __str__(self): return self.as_string() @@ -419,10 +432,10 @@ def has_bad_headers(self): if self.subject: if _has_newline(self.subject): - for linenum, line in enumerate(self.subject.split('\r\n')): + for linenum, line in enumerate(self.subject.split("\r\n")): if not line: return True - if linenum > 0 and line[0] not in '\t ': + if linenum > 0 and line[0] not in "\t ": return True if _has_newline(line): return True @@ -432,7 +445,10 @@ def has_bad_headers(self): def is_bad_headers(self): from warnings import warn - msg = 'is_bad_headers is deprecated, use the new has_bad_headers method instead.' + + msg = ( + "is_bad_headers is deprecated, use the new has_bad_headers method instead." + ) warn(DeprecationWarning(msg), stacklevel=1) return self.has_bad_headers() @@ -449,12 +465,14 @@ def add_recipient(self, recipient): self.recipients.append(recipient) - def attach(self, - filename=None, - content_type=None, - data=None, - disposition=None, - headers=None): + def attach( + self, + filename=None, + content_type=None, + data=None, + disposition=None, + headers=None, + ): """Adds an attachment to the message. :param filename: filename of attachment @@ -463,11 +481,11 @@ def attach(self, :param disposition: content-disposition (if any) """ self.attachments.append( - Attachment(filename, content_type, data, disposition, headers)) - + Attachment(filename, content_type, data, disposition, headers) + ) -class _MailMixin(object): +class _MailMixin: @contextmanager def record_messages(self): """Records all messages. Use in unit tests for example:: @@ -520,15 +538,28 @@ def connect(self): """Opens a connection to the mail host.""" app = getattr(self, "app", None) or current_app try: - return Connection(app.extensions['mail']) + return Connection(app.extensions["mail"]) except KeyError: - raise RuntimeError("The current application was not configured with Flask-Mail") + raise RuntimeError( + "The current application was not configured with Flask-Mail" + ) class _Mail(_MailMixin): - def __init__(self, server, username, password, port, use_tls, use_ssl, - default_sender, debug, max_emails, suppress, - ascii_attachments=False): + def __init__( + self, + server, + username, + password, + port, + use_tls, + use_ssl, + default_sender, + debug, + max_emails, + suppress, + ascii_attachments=False, + ): self.server = server self.username = username self.password = password @@ -557,17 +588,17 @@ def __init__(self, app=None): def init_mail(self, config, debug=False, testing=False): return _Mail( - config.get('MAIL_SERVER', '127.0.0.1'), - config.get('MAIL_USERNAME'), - config.get('MAIL_PASSWORD'), - config.get('MAIL_PORT', 25), - config.get('MAIL_USE_TLS', False), - config.get('MAIL_USE_SSL', False), - config.get('MAIL_DEFAULT_SENDER'), - int(config.get('MAIL_DEBUG', debug)), - config.get('MAIL_MAX_EMAILS'), - config.get('MAIL_SUPPRESS_SEND', testing), - config.get('MAIL_ASCII_ATTACHMENTS', False) + config.get("MAIL_SERVER", "127.0.0.1"), + config.get("MAIL_USERNAME"), + config.get("MAIL_PASSWORD"), + config.get("MAIL_PORT", 25), + config.get("MAIL_USE_TLS", False), + config.get("MAIL_USE_SSL", False), + config.get("MAIL_DEFAULT_SENDER"), + int(config.get("MAIL_DEBUG", debug)), + config.get("MAIL_MAX_EMAILS"), + config.get("MAIL_SUPPRESS_SEND", testing), + config.get("MAIL_ASCII_ATTACHMENTS", False), ) def init_app(self, app): @@ -581,8 +612,8 @@ def init_app(self, app): state = self.init_mail(app.config, app.debug, app.testing) # register extension with app - app.extensions = getattr(app, 'extensions', {}) - app.extensions['mail'] = state + app.extensions = getattr(app, "extensions", {}) + app.extensions["mail"] = state return state def __getattr__(self, name): @@ -591,7 +622,10 @@ def __getattr__(self, name): signals = blinker.Namespace() -email_dispatched = signals.signal("email-dispatched", doc=""" +email_dispatched = signals.signal( + "email-dispatched", + doc=""" Signal sent when an email is dispatched. This signal will also be sent in testing mode, even though the email will not actually be sent. -""") +""", +) diff --git a/scripts/release.py b/scripts/release.py index 69c180f..f60e57e 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,55 +1,57 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ - make-release - ~~~~~~~~~~~~ +make-release +~~~~~~~~~~~~ - Helper script that performs a release. Does pretty much everything - automatically for us. +Helper script that performs a release. Does pretty much everything +automatically for us. - :copyright: (c) 2011 by Armin Ronacher. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2011 by Armin Ronacher. +:license: BSD, see LICENSE for more details. """ -import sys + import os import re -from datetime import datetime, date -from subprocess import Popen, PIPE +import sys +from datetime import date +from datetime import datetime +from subprocess import PIPE +from subprocess import Popen -_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)') +_date_clean_re = re.compile(r"(\d+)(st|nd|rd|th)") def installed_libraries(): - return Popen(['pip', 'freeze'], stdout=PIPE).communicate()[0] + return Popen(["pip", "freeze"], stdout=PIPE).communicate()[0] def has_library_installed(library): - return library + '==' in installed_libraries() + return library + "==" in installed_libraries() def parse_changelog(): - with open('CHANGES') as f: + with open("CHANGES") as f: lineiter = iter(f) for line in lineiter: - match = re.search('^Version\s+(.*)', line.strip()) + match = re.search(r"^Version\s+(.*)", line.strip()) if match is None: continue version = match.group(1).strip() - if lineiter.next().count('-') != len(line.strip()): - fail('Invalid hyphen count below version line: %s', line.strip()) + if lineiter.next().count("-") != len(line.strip()): + fail("Invalid hyphen count below version line: %s", line.strip()) while 1: released = lineiter.next().strip() if released: break - match = re.search(r'Released (\w+\s+\d+\w+\s+\d+)', released) + match = re.search(r"Released (\w+\s+\d+\w+\s+\d+)", released) if match is None: - fail('Could not find release date in version %s' % version) + fail("Could not find release date in version %s" % version) datestr = parse_date(match.group(1).strip()) @@ -58,16 +60,16 @@ def parse_changelog(): def bump_version(version): try: - parts = map(int, version.split('.')) + parts = map(int, version.split(".")) except ValueError: - fail('Current version is not numeric') + fail("Current version is not numeric") parts[-1] += 1 - return '.'.join(map(str, parts)) + return ".".join(map(str, parts)) def parse_date(string): - string = _date_clean_re.sub(r'\1', string) - return datetime.strptime(string, '%B %d %Y') + string = _date_clean_re.sub(r"\1", string) + return datetime.strptime(string, "%B %d %Y") def set_filename_version(filename, version_number, pattern): @@ -79,37 +81,40 @@ def inject_version(match): return before + version_number + after with open(filename) as f: - contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, - inject_version, f.read()) + contents = re.sub( + r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, inject_version, f.read() + ) if not changed: - fail('Could not find %s in %s', pattern, filename) + fail("Could not find %s in %s", pattern, filename) - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(contents) def set_init_version(version): - info('Setting __init__.py version to %s', version) - set_filename_version('flask_mail.py', version, '__version__') + info("Setting __init__.py version to %s", version) + set_filename_version("flask_mail.py", version, "__version__") def set_setup_version(version): - info('Setting setup.py version to %s', version) - set_filename_version('setup.py', version, 'version') + info("Setting setup.py version to %s", version) + set_filename_version("setup.py", version, "version") def set_docs_version(version): - info('Setting docs/conf.py version to %s', version) - set_filename_version('docs/conf.py', version, 'version') + info("Setting docs/conf.py version to %s", version) + set_filename_version("docs/conf.py", version, "version") def build_and_upload(): - Popen([sys.executable, 'setup.py', 'sdist', 'build_sphinx', 'upload', 'upload_sphinx']).wait() + Popen( + [sys.executable, "setup.py", "sdist", "build_sphinx", "upload", "upload_sphinx"] + ).wait() def fail(message, *args): - print >> sys.stderr, 'Error:', message % args + print >> sys.stderr, "Error:", message % args sys.exit(1) @@ -118,22 +123,22 @@ def info(message, *args): def get_git_tags(): - return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines()) + return set(Popen(["git", "tag"], stdout=PIPE).communicate()[0].splitlines()) def git_is_clean(): - return Popen(['git', 'diff', '--quiet']).wait() == 0 + return Popen(["git", "diff", "--quiet"]).wait() == 0 def make_git_commit(message, *args): message = message % args - Popen(['git', 'commit', '-am', message]).wait() + Popen(["git", "commit", "-am", message]).wait() def make_git_tag(tag): info('Tagging "%s"', tag) - Popen(['git', 'tag', '-a', tag, '-m', '%s release' % tag]).wait() - Popen(['git', 'push', '--tags']).wait() + Popen(["git", "tag", "-a", tag, "-m", "%s release" % tag]).wait() + Popen(["git", "push", "--tags"]).wait() def update_version(version): @@ -142,48 +147,47 @@ def update_version(version): def get_branches(): - return set(Popen(['git', 'branch'], stdout=PIPE).communicate()[0].splitlines()) + return set(Popen(["git", "branch"], stdout=PIPE).communicate()[0].splitlines()) def branch_is(branch): - return '* ' + branch in get_branches() + return "* " + branch in get_branches() def main(): - os.chdir(os.path.join(os.path.dirname(__file__), '..')) + os.chdir(os.path.join(os.path.dirname(__file__), "..")) rv = parse_changelog() if rv is None: - fail('Could not parse changelog') + fail("Could not parse changelog") version, release_date = rv tags = get_git_tags() - for lib in ['Sphinx', 'Sphinx-PyPI-upload']: + for lib in ["Sphinx", "Sphinx-PyPI-upload"]: if not has_library_installed(lib): - fail('Build requires that %s be installed', lib) + fail("Build requires that %s be installed", lib) if version in tags: fail('Version "%s" is already tagged', version) if release_date.date() != date.today(): - fail('Release date is not today') + fail("Release date is not today") - if not branch_is('master'): - fail('You are not on the master branch') + if not branch_is("master"): + fail("You are not on the master branch") if not git_is_clean(): - fail('You have uncommitted changes in git') + fail("You have uncommitted changes in git") - info('Releasing %s (release date %s)', - version, release_date.strftime('%d/%m/%Y')) + info("Releasing %s (release date %s)", version, release_date.strftime("%d/%m/%Y")) update_version(version) - make_git_commit('Bump version number to %s', version) + make_git_commit("Bump version number to %s", version) make_git_tag(version) build_and_upload() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/setup.cfg b/setup.cfg index 736bfed..06842c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,4 @@ source-dir = docs/ build-dir = docs/_build [upload_sphinx] -upload-dir = docs/_build/html \ No newline at end of file +upload-dir = docs/_build/html diff --git a/setup.py b/setup.py index ae2e352..0913c67 100644 --- a/setup.py +++ b/setup.py @@ -11,37 +11,35 @@ * `documentation `_ """ -from setuptools import setup +from setuptools import setup setup( - name='Flask-Mail', - version='0.9.1', - url='https://github.com/rduplain/flask-mail', - license='BSD', - author='Dan Jacob', - author_email='danjac354@gmail.com', - maintainer='Ron DuPlain', - maintainer_email='ron.duplain@gmail.com', - description='Flask extension for sending email', + name="Flask-Mail", + version="0.9.1", + url="https://github.com/rduplain/flask-mail", + license="BSD", + author="Dan Jacob", + author_email="danjac354@gmail.com", + maintainer="Ron DuPlain", + maintainer_email="ron.duplain@gmail.com", + description="Flask extension for sending email", long_description=__doc__, - py_modules=[ - 'flask_mail' - ], + py_modules=["flask_mail"], zip_safe=False, - platforms='any', + platforms="any", install_requires=[ - 'Flask', - 'blinker', + "Flask", + "blinker", ], classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", + ], ) diff --git a/tests.py b/tests.py index 1de0308..5b2d9c5 100644 --- a/tests.py +++ b/tests.py @@ -1,25 +1,22 @@ -# -*- coding: utf-8 -*- - -from __future__ import with_statement - import base64 import email -import unittest -import time import re -import mock +import time +import unittest from contextlib import contextmanager - from email.header import Header -from email import charset +from unittest import mock from flask import Flask -from flask_mail import Mail, Message, BadHeaderError, sanitize_address, PY3 +from flask_mail import BadHeaderError +from flask_mail import Mail +from flask_mail import Message +from flask_mail import PY3 +from flask_mail import sanitize_address from speaklater import make_lazy_string class TestCase(unittest.TestCase): - TESTING = True MAIL_DEFAULT_SENDER = "support@mysite.com" @@ -53,44 +50,37 @@ def mail_config(self, **settings): setattr(state, k, v) def assertIn(self, member, container, msg=None): - if hasattr(unittest.TestCase, 'assertIn'): + if hasattr(unittest.TestCase, "assertIn"): return unittest.TestCase.assertIn(self, member, container, msg) return self.assertTrue(member in container) def assertNotIn(self, member, container, msg=None): - if hasattr(unittest.TestCase, 'assertNotIn'): + if hasattr(unittest.TestCase, "assertNotIn"): return unittest.TestCase.assertNotIn(self, member, container, msg) return self.assertFalse(member in container) def assertIsNone(self, obj, msg=None): - if hasattr(unittest.TestCase, 'assertIsNone'): + if hasattr(unittest.TestCase, "assertIsNone"): return unittest.TestCase.assertIsNone(self, obj, msg) return self.assertTrue(obj is None) def assertIsNotNone(self, obj, msg=None): - if hasattr(unittest.TestCase, 'assertIsNotNone'): + if hasattr(unittest.TestCase, "assertIsNotNone"): return unittest.TestCase.assertIsNotNone(self, obj, msg) return self.assertTrue(obj is not None) class TestInitialization(TestCase): - def test_init_mail(self): - mail = self.mail.init_mail( - self.app.config, - self.app.debug, - self.app.testing - ) + mail = self.mail.init_mail(self.app.config, self.app.debug, self.app.testing) self.assertEqual(self.mail.state.__dict__, mail.__dict__) class TestMessage(TestCase): - def test_initialize(self): - msg = Message(subject="subject", - recipients=["to@example.com"]) - self.assertEqual(msg.sender, self.app.extensions['mail'].default_sender) + msg = Message(subject="subject", recipients=["to@example.com"]) + self.assertEqual(msg.sender, self.app.extensions["mail"].default_sender) self.assertEqual(msg.recipients, ["to@example.com"]) def test_recipients_properly_initialized(self): @@ -105,15 +95,19 @@ def test_esmtp_options_properly_initialized(self): self.assertEqual(msg.mail_options, []) self.assertEqual(msg.rcpt_options, []) - msg = Message(subject="subject", mail_options=['BODY=8BITMIME']) - self.assertEqual(msg.mail_options, ['BODY=8BITMIME']) + msg = Message(subject="subject", mail_options=["BODY=8BITMIME"]) + self.assertEqual(msg.mail_options, ["BODY=8BITMIME"]) - msg2 = Message(subject="subject", rcpt_options=['NOTIFY=SUCCESS']) - self.assertEqual(msg2.rcpt_options, ['NOTIFY=SUCCESS']) + msg2 = Message(subject="subject", rcpt_options=["NOTIFY=SUCCESS"]) + self.assertEqual(msg2.rcpt_options, ["NOTIFY=SUCCESS"]) def test_sendto_properly_set(self): - msg = Message(subject="subject", recipients=["somebody@here.com"], - cc=["cc@example.com"], bcc=["bcc@example.com"]) + msg = Message( + subject="subject", + recipients=["somebody@here.com"], + cc=["cc@example.com"], + bcc=["bcc@example.com"], + ) self.assertEqual(len(msg.send_to), 3) msg.add_recipient("cc@example.com") self.assertEqual(len(msg.send_to), 3) @@ -124,243 +118,275 @@ def test_add_recipient(self): self.assertEqual(msg.recipients, ["to@example.com"]) def test_sender_as_tuple(self): - msg = Message(subject="testing", - sender=("tester", "tester@example.com")) - self.assertEqual('tester ', msg.sender) + msg = Message(subject="testing", sender=("tester", "tester@example.com")) + self.assertEqual("tester ", msg.sender) def test_default_sender_as_tuple(self): - self.app.extensions['mail'].default_sender = ('tester', 'tester@example.com') + self.app.extensions["mail"].default_sender = ("tester", "tester@example.com") msg = Message(subject="testing") - self.assertEqual('tester ', msg.sender) + self.assertEqual("tester ", msg.sender) def test_reply_to(self): - msg = Message(subject="testing", - recipients=["to@example.com"], - sender="spammer ", - reply_to="somebody ", - body="testing") + msg = Message( + subject="testing", + recipients=["to@example.com"], + sender="spammer ", + reply_to="somebody ", + body="testing", + ) response = msg.as_string() - h = Header("Reply-To: %s" % sanitize_address('somebody ')) + h = Header("Reply-To: %s" % sanitize_address("somebody ")) self.assertIn(h.encode(), str(response)) def test_send_without_sender(self): - self.app.extensions['mail'].default_sender = None + self.app.extensions["mail"].default_sender = None msg = Message(subject="testing", recipients=["to@example.com"], body="testing") self.assertRaises(AssertionError, self.mail.send, msg) def test_send_without_recipients(self): - msg = Message(subject="testing", - recipients=[], - body="testing") + msg = Message(subject="testing", recipients=[], body="testing") self.assertRaises(AssertionError, self.mail.send, msg) def test_bcc(self): - msg = Message(sender="from@example.com", - subject="testing", - recipients=["to@example.com"], - body="testing", - bcc=["tosomeoneelse@example.com"]) + msg = Message( + sender="from@example.com", + subject="testing", + recipients=["to@example.com"], + body="testing", + bcc=["tosomeoneelse@example.com"], + ) response = msg.as_string() self.assertNotIn("tosomeoneelse@example.com", str(response)) def test_cc(self): - msg = Message(sender="from@example.com", - subject="testing", - recipients=["to@example.com"], - body="testing", - cc=["tosomeoneelse@example.com"]) + msg = Message( + sender="from@example.com", + subject="testing", + recipients=["to@example.com"], + body="testing", + cc=["tosomeoneelse@example.com"], + ) response = msg.as_string() self.assertIn("Cc: tosomeoneelse@example.com", str(response)) def test_attach(self): - msg = Message(subject="testing", - recipients=["to@example.com"], - body="testing") - msg.attach(data=b"this is a test", - content_type="text/plain") + msg = Message(subject="testing", recipients=["to@example.com"], body="testing") + msg.attach(data=b"this is a test", content_type="text/plain") a = msg.attachments[0] self.assertIsNone(a.filename) - self.assertEqual(a.disposition, 'attachment') + self.assertEqual(a.disposition, "attachment") self.assertEqual(a.content_type, "text/plain") self.assertEqual(a.data, b"this is a test") def test_bad_header_subject(self): - msg = Message(subject="testing\r\n", - sender="from@example.com", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\r\n", + sender="from@example.com", + body="testing", + recipients=["to@example.com"], + ) self.assertRaises(BadHeaderError, self.mail.send, msg) def test_multiline_subject(self): - msg = Message(subject="testing\r\n testing\r\n testing \r\n \ttesting", - sender="from@example.com", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\r\n testing\r\n testing \r\n \ttesting", + sender="from@example.com", + body="testing", + recipients=["to@example.com"], + ) self.mail.send(msg) response = msg.as_string() self.assertIn("From: from@example.com", str(response)) self.assertIn("testing\r\n testing\r\n testing \r\n \ttesting", str(response)) def test_bad_multiline_subject(self): - msg = Message(subject="testing\r\n testing\r\n ", - sender="from@example.com", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\r\n testing\r\n ", + sender="from@example.com", + body="testing", + recipients=["to@example.com"], + ) self.assertRaises(BadHeaderError, self.mail.send, msg) - msg = Message(subject="testing\r\n testing\r\n\t", - sender="from@example.com", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\r\n testing\r\n\t", + sender="from@example.com", + body="testing", + recipients=["to@example.com"], + ) self.assertRaises(BadHeaderError, self.mail.send, msg) - msg = Message(subject="testing\r\n testing\r\n\n", - sender="from@example.com", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\r\n testing\r\n\n", + sender="from@example.com", + body="testing", + recipients=["to@example.com"], + ) self.assertRaises(BadHeaderError, self.mail.send, msg) def test_bad_header_sender(self): - msg = Message(subject="testing", - sender="from@example.com\r\n", - recipients=["to@example.com"], - body="testing") + msg = Message( + subject="testing", + sender="from@example.com\r\n", + recipients=["to@example.com"], + body="testing", + ) - self.assertIn('From: from@example.com', msg.as_string()) + self.assertIn("From: from@example.com", msg.as_string()) def test_bad_header_reply_to(self): - msg = Message(subject="testing", - sender="from@example.com", - reply_to="evil@example.com\r", - recipients=["to@example.com"], - body="testing") + msg = Message( + subject="testing", + sender="from@example.com", + reply_to="evil@example.com\r", + recipients=["to@example.com"], + body="testing", + ) - self.assertIn('From: from@example.com', msg.as_string()) - self.assertIn('To: to@example.com', msg.as_string()) - self.assertIn('Reply-To: evil@example.com', msg.as_string()) + self.assertIn("From: from@example.com", msg.as_string()) + self.assertIn("To: to@example.com", msg.as_string()) + self.assertIn("Reply-To: evil@example.com", msg.as_string()) def test_bad_header_recipient(self): - msg = Message(subject="testing", - sender="from@example.com", - recipients=[ - "to@example.com", - "to\r\n@example.com"], - body="testing") + msg = Message( + subject="testing", + sender="from@example.com", + recipients=["to@example.com", "to\r\n@example.com"], + body="testing", + ) - self.assertIn('To: to@example.com', msg.as_string()) + self.assertIn("To: to@example.com", msg.as_string()) def test_emails_are_sanitized(self): - msg = Message(subject="testing", - sender="sender\r\n@example.com", - reply_to="reply_to\r\n@example.com", - recipients=["recipient\r\n@example.com"]) - self.assertIn('sender@example.com', msg.as_string()) - self.assertIn('reply_to@example.com', msg.as_string()) - self.assertIn('recipient@example.com', msg.as_string()) + msg = Message( + subject="testing", + sender="sender\r\n@example.com", + reply_to="reply_to\r\n@example.com", + recipients=["recipient\r\n@example.com"], + ) + self.assertIn("sender@example.com", msg.as_string()) + self.assertIn("reply_to@example.com", msg.as_string()) + self.assertIn("recipient@example.com", msg.as_string()) def test_plain_message(self): plain_text = "Hello Joe,\nHow are you?" - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body=plain_text) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body=plain_text, + ) self.assertEqual(plain_text, msg.body) - self.assertIn('Content-Type: text/plain', msg.as_string()) + self.assertIn("Content-Type: text/plain", msg.as_string()) def test_message_str(self): - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body="some plain text") + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body="some plain text", + ) self.assertEqual(msg.as_string(), str(msg)) def test_plain_message_with_attachments(self): - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body="hello") + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body="hello", + ) - msg.attach(data=b"this is a test", - content_type="text/plain") + msg.attach(data=b"this is a test", content_type="text/plain") - self.assertIn('Content-Type: multipart/mixed', msg.as_string()) + self.assertIn("Content-Type: multipart/mixed", msg.as_string()) def test_plain_message_with_ascii_attachment(self): - msg = Message(subject="subject", - recipients=["to@example.com"], - body="hello") + msg = Message(subject="subject", recipients=["to@example.com"], body="hello") - msg.attach(data=b"this is a test", - content_type="text/plain", - filename='test doc.txt') + msg.attach( + data=b"this is a test", content_type="text/plain", filename="test doc.txt" + ) - self.assertIn('Content-Disposition: attachment; filename="test doc.txt"', msg.as_string()) + self.assertIn( + 'Content-Disposition: attachment; filename="test doc.txt"', msg.as_string() + ) def test_plain_message_with_unicode_attachment(self): - msg = Message(subject="subject", - recipients=["to@example.com"], - body="hello") + msg = Message(subject="subject", recipients=["to@example.com"], body="hello") - msg.attach(data=b"this is a test", - content_type="text/plain", - filename=u'ünicöde ←→ ✓.txt') + msg.attach( + data=b"this is a test", + content_type="text/plain", + filename="ünicöde ←→ ✓.txt", + ) parsed = email.message_from_string(msg.as_string()) - self.assertIn(re.sub(r'\s+', ' ', parsed.get_payload()[1].get('Content-Disposition')), [ - 'attachment; filename*="UTF8\'\'%C3%BCnic%C3%B6de%20%E2%86%90%E2%86%92%20%E2%9C%93.txt"', - 'attachment; filename*=UTF8\'\'%C3%BCnic%C3%B6de%20%E2%86%90%E2%86%92%20%E2%9C%93.txt' - ]) + self.assertIn( + re.sub(r"\s+", " ", parsed.get_payload()[1].get("Content-Disposition")), + [ + "attachment; filename*=\"UTF8''%C3%BCnic%C3%B6de%20%E2%86%90%E2%86%92%20%E2%9C%93.txt\"", + "attachment; filename*=UTF8''%C3%BCnic%C3%B6de%20%E2%86%90%E2%86%92%20%E2%9C%93.txt", + ], + ) def test_plain_message_with_ascii_converted_attachment(self): with self.mail_config(ascii_attachments=True): - msg = Message(subject="subject", - recipients=["to@example.com"], - body="hello") + msg = Message( + subject="subject", recipients=["to@example.com"], body="hello" + ) - msg.attach(data=b"this is a test", - content_type="text/plain", - filename=u'ünicödeß ←.→ ✓.txt') + msg.attach( + data=b"this is a test", + content_type="text/plain", + filename="ünicödeß ←.→ ✓.txt", + ) parsed = email.message_from_string(msg.as_string()) self.assertIn( 'Content-Disposition: attachment; filename="unicode . .txt"', - msg.as_string()) + msg.as_string(), + ) def test_html_message(self): html_text = "

Hello World

" - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - html=html_text) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + html=html_text, + ) self.assertEqual(html_text, msg.html) - self.assertIn('Content-Type: multipart/alternative', msg.as_string()) + self.assertIn("Content-Type: multipart/alternative", msg.as_string()) def test_json_message(self): json_text = '{"msg": "Hello World!}' - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - alts={'json': json_text}) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + alts={"json": json_text}, + ) - self.assertEqual(json_text, msg.alts['json']) - self.assertIn('Content-Type: multipart/alternative', msg.as_string()) + self.assertEqual(json_text, msg.alts["json"]) + self.assertIn("Content-Type: multipart/alternative", msg.as_string()) def test_html_message_with_attachments(self): html_text = "

Hello World

" - plain_text = 'Hello World' - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body=plain_text, - html=html_text) - msg.attach(data=b"this is a test", - content_type="text/plain") + plain_text = "Hello World" + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body=plain_text, + html=html_text, + ) + msg.attach(data=b"this is a test", content_type="text/plain") self.assertEqual(html_text, msg.html) - self.assertIn('Content-Type: multipart/alternative', msg.as_string()) + self.assertIn("Content-Type: multipart/alternative", msg.as_string()) parsed = email.message_from_string(msg.as_string()) self.assertEqual(len(parsed.get_payload()), 2) @@ -371,59 +397,73 @@ def test_html_message_with_attachments(self): plain, html = body.get_payload() self.assertEqual(plain.get_payload(), plain_text) self.assertEqual(html.get_payload(), html_text) - self.assertEqual(base64.b64decode(attachment.get_payload()), b'this is a test') + self.assertEqual(base64.b64decode(attachment.get_payload()), b"this is a test") def test_date_header(self): before = time.time() - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body="hello", - date=time.time()) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body="hello", + date=time.time(), + ) after = time.time() self.assertTrue(before <= msg.date <= after) dateFormatted = email.utils.formatdate(msg.date, localtime=True) - self.assertIn('Date: ' + dateFormatted, msg.as_string()) + self.assertIn("Date: " + dateFormatted, msg.as_string()) def test_msgid_header(self): - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body="hello") + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body="hello", + ) # see RFC 5322 section 3.6.4. for the exact format specification r = re.compile(r"<\S+@\S+>").match(msg.msgId) self.assertIsNotNone(r) - self.assertIn('Message-ID: ' + msg.msgId, msg.as_string()) + self.assertIn("Message-ID: " + msg.msgId, msg.as_string()) def test_unicode_sender_tuple(self): - msg = Message(subject="subject", - sender=(u"ÄÜÖ → ✓", 'from@example.com>'), - recipients=["to@example.com"]) + msg = Message( + subject="subject", + sender=("ÄÜÖ → ✓", "from@example.com>"), + recipients=["to@example.com"], + ) - self.assertIn('From: =?utf-8?b?w4TDnMOWIOKGkiDinJM=?= ', msg.as_string()) + self.assertIn( + "From: =?utf-8?b?w4TDnMOWIOKGkiDinJM=?= ", msg.as_string() + ) def test_unicode_sender(self): - msg = Message(subject="subject", - sender=u'ÄÜÖ → ✓ >', - recipients=["to@example.com"]) + msg = Message( + subject="subject", + sender="ÄÜÖ → ✓ >", + recipients=["to@example.com"], + ) - self.assertIn('From: =?utf-8?b?w4TDnMOWIOKGkiDinJM=?= ', msg.as_string()) + self.assertIn( + "From: =?utf-8?b?w4TDnMOWIOKGkiDinJM=?= ", msg.as_string() + ) def test_unicode_headers(self): - msg = Message(subject="subject", - sender=u'ÄÜÖ → ✓ ', - recipients=[u"Ä ", u"Ü "], - cc=[u"Ö "]) + msg = Message( + subject="subject", + sender="ÄÜÖ → ✓ ", + recipients=["Ä ", "Ü "], + cc=["Ö "], + ) response = msg.as_string() - a1 = sanitize_address(u"Ä ") - a2 = sanitize_address(u"Ü ") + a1 = sanitize_address("Ä ") + a2 = sanitize_address("Ü ") h1_a = Header("To: %s, %s" % (a1, a2)) h1_b = Header("To: %s, %s" % (a2, a1)) - h2 = Header("From: %s" % sanitize_address(u"ÄÜÖ → ✓ ")) - h3 = Header("Cc: %s" % sanitize_address(u"Ö ")) + h2 = Header("From: %s" % sanitize_address("ÄÜÖ → ✓ ")) + h3 = Header("Cc: %s" % sanitize_address("Ö ")) # Ugly, but there's no guaranteed order of the recipients in the header try: @@ -435,211 +475,233 @@ def test_unicode_headers(self): self.assertIn(h3.encode(), response) def test_unicode_subject(self): - msg = Message(subject=make_lazy_string(lambda a: a, u"sübject"), - sender='from@example.com', - recipients=["to@example.com"]) - self.assertIn('=?utf-8?q?s=C3=BCbject?=', msg.as_string()) + msg = Message( + subject=make_lazy_string(lambda a: a, "sübject"), + sender="from@example.com", + recipients=["to@example.com"], + ) + self.assertIn("=?utf-8?q?s=C3=BCbject?=", msg.as_string()) def test_extra_headers(self): - msg = Message(sender="from@example.com", - subject="subject", - recipients=["to@example.com"], - body="hello", - extra_headers={'X-Extra-Header': 'Yes'}) - self.assertIn('X-Extra-Header: Yes', msg.as_string()) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["to@example.com"], + body="hello", + extra_headers={"X-Extra-Header": "Yes"}, + ) + self.assertIn("X-Extra-Header: Yes", msg.as_string()) def test_message_charset(self): - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"], - charset='us-ascii') + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["foo@bar.com"], + charset="us-ascii", + ) # ascii body msg.body = "normal ascii text" self.assertIn('Content-Type: text/plain; charset="us-ascii"', msg.as_string()) # ascii html - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"], - charset='us-ascii') + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["foo@bar.com"], + charset="us-ascii", + ) msg.body = None msg.html = "

hello

" self.assertIn('Content-Type: text/html; charset="us-ascii"', msg.as_string()) # unicode body - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"]) - msg.body = u"ünicöde ←→ ✓" + msg = Message( + sender="from@example.com", subject="subject", recipients=["foo@bar.com"] + ) + msg.body = "ünicöde ←→ ✓" self.assertIn('Content-Type: text/plain; charset="utf-8"', msg.as_string()) # unicode body and unicode html - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"]) - msg.html = u"ünicöde ←→ ✓" + msg = Message( + sender="from@example.com", subject="subject", recipients=["foo@bar.com"] + ) + msg.html = "ünicöde ←→ ✓" self.assertIn('Content-Type: text/plain; charset="utf-8"', msg.as_string()) self.assertIn('Content-Type: text/html; charset="utf-8"', msg.as_string()) # unicode body and attachments - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"]) + msg = Message( + sender="from@example.com", subject="subject", recipients=["foo@bar.com"] + ) msg.html = None - msg.attach(data=b"foobar", content_type='text/csv') + msg.attach(data=b"foobar", content_type="text/csv") self.assertIn('Content-Type: text/plain; charset="utf-8"', msg.as_string()) # unicode sender as tuple - msg = Message(sender=(u"送信者", "from@example.com"), - subject=u"表題", - recipients=["foo@bar.com"], - reply_to=u"返信先 ", - charset='shift_jis') # japanese - msg.body = u'内容' - self.assertIn('From: =?iso-2022-jp?', msg.as_string()) - self.assertNotIn('From: =?utf-8?', msg.as_string()) - self.assertIn('Subject: =?iso-2022-jp?', msg.as_string()) - self.assertNotIn('Subject: =?utf-8?', msg.as_string()) - self.assertIn('Reply-To: =?iso-2022-jp?', msg.as_string()) - self.assertNotIn('Reply-To: =?utf-8?', msg.as_string()) - self.assertIn('Content-Type: text/plain; charset="iso-2022-jp"', msg.as_string()) + msg = Message( + sender=("送信者", "from@example.com"), + subject="表題", + recipients=["foo@bar.com"], + reply_to="返信先 ", + charset="shift_jis", + ) # japanese + msg.body = "内容" + self.assertIn("From: =?iso-2022-jp?", msg.as_string()) + self.assertNotIn("From: =?utf-8?", msg.as_string()) + self.assertIn("Subject: =?iso-2022-jp?", msg.as_string()) + self.assertNotIn("Subject: =?utf-8?", msg.as_string()) + self.assertIn("Reply-To: =?iso-2022-jp?", msg.as_string()) + self.assertNotIn("Reply-To: =?utf-8?", msg.as_string()) + self.assertIn( + 'Content-Type: text/plain; charset="iso-2022-jp"', msg.as_string() + ) # unicode subject sjis - msg = Message(sender="from@example.com", - subject=u"表題", - recipients=["foo@bar.com"], - charset='shift_jis') # japanese - msg.body = u'内容' - self.assertIn('Subject: =?iso-2022-jp?', msg.as_string()) - self.assertIn('Content-Type: text/plain; charset="iso-2022-jp"', msg.as_string()) + msg = Message( + sender="from@example.com", + subject="表題", + recipients=["foo@bar.com"], + charset="shift_jis", + ) # japanese + msg.body = "内容" + self.assertIn("Subject: =?iso-2022-jp?", msg.as_string()) + self.assertIn( + 'Content-Type: text/plain; charset="iso-2022-jp"', msg.as_string() + ) # unicode subject utf-8 - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"], - charset='utf-8') - msg.body = u'内容' - self.assertIn('Subject: subject', msg.as_string()) + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["foo@bar.com"], + charset="utf-8", + ) + msg.body = "内容" + self.assertIn("Subject: subject", msg.as_string()) self.assertIn('Content-Type: text/plain; charset="utf-8"', msg.as_string()) # ascii subject - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"], - charset='us-ascii') + msg = Message( + sender="from@example.com", + subject="subject", + recipients=["foo@bar.com"], + charset="us-ascii", + ) msg.body = "normal ascii text" - self.assertNotIn('Subject: =?us-ascii?', msg.as_string()) + self.assertNotIn("Subject: =?us-ascii?", msg.as_string()) self.assertIn('Content-Type: text/plain; charset="us-ascii"', msg.as_string()) # default charset - msg = Message(sender="from@example.com", - subject="subject", - recipients=["foo@bar.com"]) + msg = Message( + sender="from@example.com", subject="subject", recipients=["foo@bar.com"] + ) msg.body = "normal ascii text" - self.assertNotIn('Subject: =?', msg.as_string()) + self.assertNotIn("Subject: =?", msg.as_string()) self.assertIn('Content-Type: text/plain; charset="utf-8"', msg.as_string()) def test_empty_subject_header(self): - msg = Message(sender="from@example.com", - recipients=["foo@bar.com"]) + msg = Message(sender="from@example.com", recipients=["foo@bar.com"]) msg.body = "normal ascii text" self.mail.send(msg) - self.assertNotIn('Subject:', msg.as_string()) + self.assertNotIn("Subject:", msg.as_string()) -class TestMail(TestCase): +class TestMail(TestCase): def test_send(self): - with self.mail.record_messages() as outbox: - msg = Message(subject="testing", - recipients=["tester@example.com"], - body="test") + msg = Message( + subject="testing", recipients=["tester@example.com"], body="test" + ) self.mail.send(msg) self.assertIsNotNone(msg.date) self.assertEqual(len(outbox), 1) sent_msg = outbox[0] - self.assertEqual(msg.sender, self.app.extensions['mail'].default_sender) + self.assertEqual(msg.sender, self.app.extensions["mail"].default_sender) def test_send_message(self): - with self.mail.record_messages() as outbox: - self.mail.send_message(subject="testing", - recipients=["tester@example.com"], - body="test") + self.mail.send_message( + subject="testing", recipients=["tester@example.com"], body="test" + ) self.assertEqual(len(outbox), 1) msg = outbox[0] self.assertEqual(msg.subject, "testing") self.assertEqual(msg.recipients, ["tester@example.com"]) self.assertEqual(msg.body, "test") - self.assertEqual(msg.sender, self.app.extensions['mail'].default_sender) + self.assertEqual(msg.sender, self.app.extensions["mail"].default_sender) class TestConnection(TestCase): - def test_send_message(self): with self.mail.record_messages() as outbox: with self.mail.connect() as conn: - conn.send_message(subject="testing", - recipients=["to@example.com"], - body="testing") + conn.send_message( + subject="testing", recipients=["to@example.com"], body="testing" + ) self.assertEqual(len(outbox), 1) sent_msg = outbox[0] - self.assertEqual(sent_msg.sender, self.app.extensions['mail'].default_sender) + self.assertEqual( + sent_msg.sender, self.app.extensions["mail"].default_sender + ) def test_send_single(self): with self.mail.record_messages() as outbox: with self.mail.connect() as conn: - msg = Message(subject="testing", - recipients=["to@example.com"], - body="testing") + msg = Message( + subject="testing", recipients=["to@example.com"], body="testing" + ) conn.send(msg) self.assertEqual(len(outbox), 1) sent_msg = outbox[0] self.assertEqual(sent_msg.subject, "testing") self.assertEqual(sent_msg.recipients, ["to@example.com"]) self.assertEqual(sent_msg.body, "testing") - self.assertEqual(sent_msg.sender, self.app.extensions['mail'].default_sender) + self.assertEqual( + sent_msg.sender, self.app.extensions["mail"].default_sender + ) def test_send_many(self): with self.mail.record_messages() as outbox: with self.mail.connect() as conn: for i in range(100): - msg = Message(subject="testing", - recipients=["to@example.com"], - body="testing") + msg = Message( + subject="testing", recipients=["to@example.com"], body="testing" + ) conn.send(msg) self.assertEqual(len(outbox), 100) sent_msg = outbox[0] - self.assertEqual(sent_msg.sender, self.app.extensions['mail'].default_sender) + self.assertEqual( + sent_msg.sender, self.app.extensions["mail"].default_sender + ) def test_send_without_sender(self): - self.app.extensions['mail'].default_sender = None + self.app.extensions["mail"].default_sender = None msg = Message(subject="testing", recipients=["to@example.com"], body="testing") with self.mail.connect() as conn: self.assertRaises(AssertionError, conn.send, msg) def test_send_without_recipients(self): - msg = Message(subject="testing", - recipients=[], - body="testing") + msg = Message(subject="testing", recipients=[], body="testing") with self.mail.connect() as conn: self.assertRaises(AssertionError, conn.send, msg) def test_bad_header_subject(self): - msg = Message(subject="testing\n\r", - body="testing", - recipients=["to@example.com"]) + msg = Message( + subject="testing\n\r", body="testing", recipients=["to@example.com"] + ) with self.mail.connect() as conn: self.assertRaises(BadHeaderError, conn.send, msg) def test_sendmail_with_ascii_recipient(self): with self.mail.connect() as conn: - with mock.patch.object(conn, 'host') as host: - msg = Message(subject="testing", - sender="from@example.com", - recipients=["to@example.com"], - body="testing") + with mock.patch.object(conn, "host") as host: + msg = Message( + subject="testing", + sender="from@example.com", + recipients=["to@example.com"], + body="testing", + ) conn.send(msg) host.sendmail.assert_called_once_with( @@ -647,16 +709,18 @@ def test_sendmail_with_ascii_recipient(self): ["to@example.com"], msg.as_bytes() if PY3 else msg.as_string(), msg.mail_options, - msg.rcpt_options + msg.rcpt_options, ) def test_sendmail_with_non_ascii_recipient(self): with self.mail.connect() as conn: - with mock.patch.object(conn, 'host') as host: - msg = Message(subject="testing", - sender="from@example.com", - recipients=[u'ÄÜÖ → ✓ '], - body="testing") + with mock.patch.object(conn, "host") as host: + msg = Message( + subject="testing", + sender="from@example.com", + recipients=["ÄÜÖ → ✓ "], + body="testing", + ) conn.send(msg) host.sendmail.assert_called_once_with( @@ -664,16 +728,18 @@ def test_sendmail_with_non_ascii_recipient(self): ["=?utf-8?b?w4TDnMOWIOKGkiDinJM=?= "], msg.as_bytes() if PY3 else msg.as_string(), msg.mail_options, - msg.rcpt_options + msg.rcpt_options, ) def test_sendmail_with_ascii_body(self): with self.mail.connect() as conn: - with mock.patch.object(conn, 'host') as host: - msg = Message(subject="testing", - sender="from@example.com", - recipients=["to@example.com"], - body="body") + with mock.patch.object(conn, "host") as host: + msg = Message( + subject="testing", + sender="from@example.com", + recipients=["to@example.com"], + body="body", + ) conn.send(msg) host.sendmail.assert_called_once_with( @@ -681,16 +747,18 @@ def test_sendmail_with_ascii_body(self): ["to@example.com"], msg.as_bytes() if PY3 else msg.as_string(), msg.mail_options, - msg.rcpt_options + msg.rcpt_options, ) def test_sendmail_with_non_ascii_body(self): with self.mail.connect() as conn: - with mock.patch.object(conn, 'host') as host: - msg = Message(subject="testing", - sender="from@example.com", - recipients=["to@example.com"], - body=u"Öö") + with mock.patch.object(conn, "host") as host: + msg = Message( + subject="testing", + sender="from@example.com", + recipients=["to@example.com"], + body="Öö", + ) conn.send(msg) @@ -699,5 +767,5 @@ def test_sendmail_with_non_ascii_body(self): ["to@example.com"], msg.as_bytes() if PY3 else msg.as_string(), msg.mail_options, - msg.rcpt_options + msg.rcpt_options, )