diff --git a/.travis.yml b/.travis.yml index 5ccb348..10d454a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ deploy: NlRpcnc3RlAxbXdxNjl0T2tnY0VybFlCRytKcFdtUUNaWFpManpWRWhLZ25jaHhmSjFUM3V2RUtH VUVKZUtZYUZtSUFCYlFwenp4UWZDNkRtbzRiM3BFNzNhSUhoVDNsOFRvZHBreHYrNWRXNDh5OVk9 true: - python: 3.5 + python: 3.6 repo: abkfenris/email_to tags: true install: pip install -U tox-travis diff --git a/README.rst b/README.rst index 5f74db6..de8483e 100644 --- a/README.rst +++ b/README.rst @@ -24,6 +24,23 @@ Simplyify sending HTML emails * Free software: MIT license * Documentation: https://email-to.readthedocs.io. +Judgement rendered by: + +.. image:: https://api.codacy.com/project/badge/Grade/7dddc6b7000349958d485080f3dda7c1 + :target: https://www.codacy.com/app/abk/email_to?utm_source=github.com&utm_medium=referral&utm_content=abkfenris/email_to&utm_campaign=Badge_Grade + :alt: Codacy + +.. image:: https://landscape.io/github/abkfenris/email_to/master/landscape.svg?style=flat + :target: https://landscape.io/github/abkfenris/email_to/master + :alt: Code Health + +.. image:: https://codeclimate.com/github/abkfenris/email_to/badges/gpa.svg + :target: https://codeclimate.com/github/abkfenris/email_to + :alt: Code Climate + +.. image:: https://scrutinizer-ci.com/g/abkfenris/email_to/badges/quality-score.png?b=master + :target: https://scrutinizer-ci.com/g/abkfenris/email_to/ + :alt: scrutinizer Features -------- diff --git a/docs/conf.py b/docs/conf.py index 33df037..5c80649 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,7 +40,10 @@ # 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.viewcode'] +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinxcontrib.napoleon' #http://sphinxcontrib-napoleon.readthedocs.org/en/latest/ + ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/email_to/__init__.py b/email_to/__init__.py index 3206d16..2045260 100644 --- a/email_to/__init__.py +++ b/email_to/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Top-level package for Email To.""" +from __future__ import absolute_import from email_to.email_to import EmailServer, Message diff --git a/email_to/email_to.py b/email_to/email_to.py index 633454f..e0566cc 100644 --- a/email_to/email_to.py +++ b/email_to/email_to.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Main module.""" - +from __future__ import absolute_import from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import smtplib @@ -18,18 +18,15 @@ class Message(object): an email, you can build a Message by adding lines of Markdown formatted strings. + args: + body (str, bytes, iterable of strings): The body of the email. + It does not need to be complete, as you can add to it later. + style (str): CSS formatting to be applied to HTML formatted verion + of email. + server (EmailServer): An EmailServer instance allowing a message + to be sent directly from it's `send` method. """ def __init__(self, body=None, style=None, server=None): - """ - Args: - body (str, bytes, iterable of strings): The body of the email. - It does not need to be complete, as you can add to it later. - style (str): CSS formatting to be applied to HTML formatted verion - of email. - server (EmailServer): An EmailServer instance allowing a message - to be sent directly from it's `send` method. - - """ if body is None: self.body = [] else: @@ -57,7 +54,7 @@ def __str__(self): def __repr__(self): return ''.format( - self.body[0], (len(self.body) - 1)) + self.body[0], (len(self.body) - 1)) def mime(self): """ Returns a MIMEMultipart message """ @@ -68,23 +65,33 @@ def mime(self): return msg - def send(self, to, subject): + def send(self, send_to, subject): """ Sends the formatted message to given recripient args: - to (:obj:str, iterable of :obj:`str`): Email addresses to send to + send_to (:obj:`str`, iterable of :obj:`str`): Email addresses to + send to subject (str): Subject line of email to send """ - self.server.send_message(self, to, subject) + self.server.send_message(self, send_to, subject) class EmailServer(object): + """ Connection to a specific email server + + args: + url (str): URL for the SMTP server + port (int): SMTP port + email_address (str): Email address to log into server and send from + password (str): Password for email address on SMTP server + """ def __init__(self, url, port, email, password): self.url = url self.port = port self.email_address = email self.password = password + self.server = None def _login(self): self.server = smtplib.SMTP(self.url, self.port) @@ -94,22 +101,46 @@ def _login(self): def _logout(self): self.server.quit() - def quick_email(self, to, subject, body, style=None): + def quick_email(self, send_to, subject, body, style=None): + """ Compose and send an email in a single call + + args: + send_to (str): + subject (str): + body (:obj:`str`, iterable of :obj:`str`): Markdown formatted + string or iterable of strings composing the message body + style (str): CSS formatting for the message + """ message = Message(body, style=style) - self.send_message(message, to, subject) + self.send_message(message, send_to, subject) + + def send_message(self, message, send_to, subject): + """ Send a precomposed Message - def send_message(self, message, to, subject): + args: + message (Message): Completed message to send + send_to (str): Email address to send the message to + subject (str): Subject of the email + """ message = message.mime() message['From'] = self.email_address - message['To'] = to + message['To'] = send_to message['Subject'] = subject self._login() - self.server.sendmail(self.email_address, to, message.as_string()) + self.server.sendmail(self.email_address, send_to, message.as_string()) self._logout() def message(self, body=None, style=None): + """ Returns a Message object + + args: + body (str, bytes, iterable of strings): The body of the email. + It does not need to be complete, as you can add to it later. + style (str): CSS formatting to be applied to HTML formatted verion + of email. + """ return Message(body=body, style=style, server=self) diff --git a/requirements_dev.txt b/requirements_dev.txt index 014b44a..49ef818 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,6 +6,7 @@ flake8==2.6.0 tox==2.3.1 coverage==4.1 Sphinx==1.4.8 +sphinxcontrib-napoleon cryptography==1.7 PyYAML==3.11 pytest==2.9.2 diff --git a/setup.py b/setup.py index 44b21ed..6796583 100755 --- a/setup.py +++ b/setup.py @@ -14,17 +14,17 @@ requirements = [ 'markdown', 'premailer' - # TODO: put package requirements here + # put package requirements here ] setup_requirements = [ 'pytest-runner', - # TODO(abkfenris): put setup requirements (distutils extensions, etc.) here + # put setup requirements (distutils extensions, etc.) here ] test_requirements = [ 'pytest', - # TODO: put package test requirements here + # put package test requirements here ] setup( diff --git a/tox.ini b/tox.ini index c3a4949..05d66c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] -envlist = py26, py27, py33, py34, py35, flake8 +envlist = py26, py27, py33, py34, py35, py36, flake8 [travis] python = + 3.6: py36 3.5: py35 3.4: py34 3.3: py33 diff --git a/travis_pypi_setup.py b/travis_pypi_setup.py deleted file mode 100644 index b4555f3..0000000 --- a/travis_pypi_setup.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Update encrypted deploy password in Travis config file.""" - - -from __future__ import print_function -import base64 -import json -import os -from getpass import getpass -import yaml -from cryptography.hazmat.primitives.serialization import load_pem_public_key -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 - - -try: - from urllib import urlopen -except ImportError: - from urllib.request import urlopen - - -GITHUB_REPO = 'abkfenris/email_to' -TRAVIS_CONFIG_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), '.travis.yml') - - -def load_key(pubkey): - """Load public RSA key. - - Work around keys with incorrect header/footer format. - - Read more about RSA encryption with cryptography: - https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ - """ - try: - return load_pem_public_key(pubkey.encode(), default_backend()) - except ValueError: - # workaround for https://github.com/travis-ci/travis-api/issues/196 - pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') - return load_pem_public_key(pubkey.encode(), default_backend()) - - -def encrypt(pubkey, password): - """Encrypt password using given RSA public key and encode it with base64. - - The encrypted password can only be decrypted by someone with the - private key (in this case, only Travis). - """ - key = load_key(pubkey) - encrypted_password = key.encrypt(password, PKCS1v15()) - return base64.b64encode(encrypted_password) - - -def fetch_public_key(repo): - """Download RSA public key Travis will use for this repo. - - Travis API docs: http://docs.travis-ci.com/api/#repository-keys - """ - keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) - data = json.loads(urlopen(keyurl).read().decode()) - if 'key' not in data: - errmsg = "Could not find public key for repo: {}.\n".format(repo) - errmsg += "Have you already added your GitHub repo to Travis?" - raise ValueError(errmsg) - return data['key'] - - -def prepend_line(filepath, line): - """Rewrite a file adding a line to its beginning.""" - with open(filepath) as f: - lines = f.readlines() - - lines.insert(0, line) - - with open(filepath, 'w') as f: - f.writelines(lines) - - -def load_yaml_config(filepath): - """Load yaml config file at the given path.""" - with open(filepath) as f: - return yaml.load(f) - - -def save_yaml_config(filepath, config): - """Save yaml config file at the given path.""" - with open(filepath, 'w') as f: - yaml.dump(config, f, default_flow_style=False) - - -def update_travis_deploy_password(encrypted_password): - """Put `encrypted_password` into the deploy section of .travis.yml.""" - config = load_yaml_config(TRAVIS_CONFIG_FILE) - - config['deploy']['password'] = dict(secure=encrypted_password) - - save_yaml_config(TRAVIS_CONFIG_FILE, config) - - line = ('# This file was autogenerated and will overwrite' - ' each time you run travis_pypi_setup.py\n') - prepend_line(TRAVIS_CONFIG_FILE, line) - - -def main(args): - """Add a PyPI password to .travis.yml so that Travis can deploy to PyPI. - - Fetch the Travis public key for the repo, and encrypt the PyPI password - with it before adding, so that only Travis can decrypt and use the PyPI - password. - """ - public_key = fetch_public_key(args.repo) - password = args.password or getpass('PyPI password: ') - update_travis_deploy_password(encrypt(public_key, password.encode())) - print("Wrote encrypted password to .travis.yml -- you're ready to deploy") - - -if '__main__' == __name__: - import argparse - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--repo', default=GITHUB_REPO, - help='GitHub repo (default: %s)' % GITHUB_REPO) - parser.add_argument('--password', - help='PyPI password (will prompt if not provided)') - - args = parser.parse_args() - main(args)