├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── api │ ├── conn.rst │ ├── connstack.rst │ └── envelope.rst ├── conf.py ├── examples │ ├── celery.rst │ └── flask.rst ├── index.rst └── make.bat ├── envelopes ├── __init__.py ├── compat.py ├── conn.py ├── connstack.py ├── envelope.py └── local.py ├── examples ├── __init__.py ├── example_celery.py └── example_flask.py ├── lib ├── __init__.py └── testing.py ├── setup.py └── tests ├── test_conn.py └── test_envelope.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | *.swp 5 | build/ 6 | dist/ 7 | Envelopes.egg-info/ 8 | MANIFEST 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | script: nosetests 7 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Envelopes is written and maintained by Tomasz Wójcik and various contributors: 2 | 3 | Lead Developer 4 | ============== 5 | 6 | * `Tomasz Wójcik `_ 7 | 8 | Patches and Suggestions 9 | ======================= 10 | 11 | * `Carter Sande `_ 12 | * `Paul Durivage `_ 13 | * `Ram Rachum `_ 14 | * `Wade Simmons `_ 15 | * `Adam Canady `_ 16 | * `Mark Lewandowski `_ 17 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Envelopes Changelog 2 | =================== 3 | 4 | Version 0.4 5 | ----------- 6 | 7 | Published on 2013-11-10 8 | 9 | * Closes `#10 `_. 10 | * Closes `#11 `_. 11 | * Closes `#12 `_. 12 | * Closes `#13 `_. 13 | * Closes `#15 `_. 14 | * Closes `#16 `_. 15 | 16 | Version 0.3 17 | ----------- 18 | 19 | Published on 2013-08-19 20 | 21 | * Closes `#6 `_. 22 | * Closes `#5 `_. 23 | 24 | Version 0.2 25 | ----------- 26 | 27 | Published on 2013-08-10 28 | 29 | * Closes `#3 `_. 30 | * Closes `#1 `_. 31 | 32 | Version 0.1.1 33 | ------------- 34 | 35 | Published on 2013-08-06 36 | 37 | * Fixes for PyPI. 38 | 39 | Version 0.1 40 | ----------- 41 | 42 | Published on 2013-08-06 43 | 44 | * Initial version. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tomasz Wójcik 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include README.rst 3 | include LICENSE 4 | include CHANGES.rst 5 | recursive-include docs * 6 | recursive-include examples * 7 | recursive-include lib * 8 | recursive-include tests * 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Envelopes 2 | ========= 3 | 4 | .. image:: https://travis-ci.org/tomekwojcik/envelopes.png?branch=master 5 | :target: https://travis-ci.org/tomekwojcik/envelopes 6 | 7 | Mailing for human beings. 8 | 9 | About 10 | ----- 11 | 12 | Envelopes is a wrapper for Python's *email* and *smtplib* modules. It aims to 13 | make working with outgoing e-mail in Python simple and fun. 14 | 15 | Short example 16 | ------------- 17 | 18 | .. sourcecode:: python 19 | 20 | from envelopes import Envelope, GMailSMTP 21 | 22 | envelope = Envelope( 23 | from_addr=(u'from@example.com', u'From Example'), 24 | to_addr=(u'to@example.com', u'To Example'), 25 | subject=u'Envelopes demo', 26 | text_body=u"I'm a helicopter!" 27 | ) 28 | envelope.add_attachment('/Users/bilbo/Pictures/helicopter.jpg') 29 | 30 | # Send the envelope using an ad-hoc connection... 31 | envelope.send('smtp.googlemail.com', login='from@example.com', 32 | password='password', tls=True) 33 | 34 | # Or send the envelope using a shared GMail connection... 35 | gmail = GMailSMTP('from@example.com', 'password') 36 | gmail.send(envelope) 37 | 38 | Features 39 | -------- 40 | 41 | Envelopes allows you to easily: 42 | 43 | * Provide e-mail addresses with or without name part. 44 | * Set text, HTML or both bodies according to your needs. 45 | * Provide any number of CC and BCC addresses. 46 | * Set standard (e.g. ``Reply-To``) and custom (e.g. ``X-Mailer``) headers. 47 | * Attach files of any kind without hassle. 48 | * Use any charset natively supported by Python's *unicode* type in addresses, 49 | bodies, headers and attachment file names. 50 | 51 | Project status 52 | -------------- 53 | 54 | This project should be considered **beta**. Proceed with caution if you decide 55 | to use Envelopes in production. 56 | 57 | Envelopes has been developed and tested with Python 2.7. Currently, Envelopes 58 | supports Python 2.6, 2.7 and 3.3. 59 | 60 | Author 61 | ------ 62 | 63 | Envelopes is developed by `Tomasz Wójcik `_. 64 | 65 | License 66 | ------- 67 | 68 | Envelopes is licensed under the MIT License. 69 | 70 | Source code and issues 71 | ---------------------- 72 | 73 | Source code is available on GitHub at: 74 | `tomekwojcik/envelopes `_. 75 | 76 | To file issue reports and feature requests use the project's issue tracker on 77 | GitHub. 78 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Envelopes.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Envelopes.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Envelopes" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Envelopes" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api/conn.rst: -------------------------------------------------------------------------------- 1 | SMTP connection 2 | =============== 3 | 4 | .. autoclass:: envelopes.conn.SMTP 5 | :members: 6 | :undoc-members: 7 | 8 | .. autoclass:: envelopes.conn.GMailSMTP 9 | :members: 10 | :undoc-members: 11 | 12 | .. autoclass:: envelopes.conn.SendGridSMTP 13 | :members: 14 | :undoc-members: 15 | 16 | .. autoclass:: envelopes.conn.MailcatcherSMTP 17 | :members: 18 | :undoc-members: 19 | -------------------------------------------------------------------------------- /docs/api/connstack.rst: -------------------------------------------------------------------------------- 1 | Connection stack 2 | ================ 3 | 4 | The connection stack allows you to use Envelopes' SMTP connection wrapper in 5 | threaded apps. Consult the example Flask app to see it in action. 6 | 7 | Code of this module has been adapted from `RQ `_ by 8 | `Vincent Driessen `_. 9 | 10 | .. autofunction:: envelopes.connstack.get_current_connection 11 | 12 | .. autofunction:: envelopes.connstack.pop_connection 13 | 14 | .. autofunction:: envelopes.connstack.push_connection 15 | 16 | .. autofunction:: envelopes.connstack.resolve_connection 17 | 18 | .. autofunction:: envelopes.connstack.use_connection 19 | -------------------------------------------------------------------------------- /docs/api/envelope.rst: -------------------------------------------------------------------------------- 1 | Envelope class 2 | ============== 3 | 4 | .. autoclass:: envelopes.envelope.Envelope 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Envelopes documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Aug 6 09:11:14 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Envelopes' 44 | copyright = u'2013, Tomasz Wójcik' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.4' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.4' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | # If true, keep warnings as "system message" paragraphs in the built documents. 90 | #keep_warnings = False 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'nature' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'Envelopesdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'Envelopes.tex', u'Envelopes Documentation', 190 | u'Tomasz Wójcik', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'envelopes', u'Envelopes Documentation', 220 | [u'Tomasz Wójcik'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'Envelopes', u'Envelopes Documentation', 234 | u'Tomasz Wójcik', 'Envelopes', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | # If true, do not generate a @detailmenu in the "Top" node's menu. 248 | #texinfo_no_detailmenu = False 249 | -------------------------------------------------------------------------------- /docs/examples/celery.rst: -------------------------------------------------------------------------------- 1 | Envelopes in Celery example 2 | --------------------------- 3 | 4 | The following code is an example of using Envelopes in Celery apps. 5 | 6 | .. sourcecode:: python 7 | 8 | from celery import Celery 9 | from envelopes import Envelope 10 | 11 | celery = Celery('envelopes_demo') 12 | celery.conf.BROKER_URL = 'amqp://guest@localhost//' 13 | 14 | 15 | @celery.task 16 | def send_envelope(): 17 | envelope = Envelope( 18 | from_addr='%s@localhost' % os.getlogin(), 19 | to_addr='%s@localhost' % os.getlogin(), 20 | subject='Envelopes in Celery demo', 21 | text_body="I'm a helicopter!" 22 | ) 23 | envelope.send('localhost', port=1025) 24 | -------------------------------------------------------------------------------- /docs/examples/flask.rst: -------------------------------------------------------------------------------- 1 | Envelopes in Flask example 2 | -------------------------- 3 | 4 | The following code is an example of using Envelopes in Flask apps. 5 | 6 | **NOTE**: Due to Flask's threaded nature it's important to wrap 7 | :py:class:`envelopes.conn.SMTP` object in connection stack. 8 | 9 | .. sourcecode:: python 10 | 11 | from envelopes import Envelope, SMTP 12 | import envelopes.connstack 13 | from flask import Flask, jsonify 14 | import os 15 | 16 | 17 | app = Flask(__name__) 18 | app.config['DEBUG'] = True 19 | 20 | conn = SMTP('127.0.0.1', 1025) 21 | 22 | 23 | @app.before_request 24 | def app_before_request(): 25 | envelopes.connstack.push_connection(conn) 26 | 27 | 28 | @app.after_request 29 | def app_after_request(response): 30 | envelopes.connstack.pop_connection() 31 | return response 32 | 33 | 34 | @app.route('/mail', methods=['POST']) 35 | def post_mail(): 36 | envelope = Envelope( 37 | from_addr='%s@localhost' % os.getlogin(), 38 | to_addr='%s@localhost' % os.getlogin(), 39 | subject='Envelopes in Flask demo', 40 | text_body="I'm a helicopter!" 41 | ) 42 | 43 | smtp = envelopes.connstack.get_current_connection() 44 | smtp.send(envelope) 45 | 46 | return jsonify(dict(status='ok')) 47 | 48 | if __name__ == '__main__': 49 | app.run() 50 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Envelopes 2 | ========= 3 | 4 | Mailing for human beings. 5 | 6 | About 7 | ----- 8 | 9 | Envelopes is a wrapper for Python's *email* and *smtplib* modules. It aims to 10 | make working with outgoing e-mail in Python simple and fun. 11 | 12 | Short example 13 | ------------- 14 | 15 | .. sourcecode:: python 16 | 17 | from envelopes import Envelope, GMailSMTP 18 | 19 | envelope = Envelope( 20 | from_addr=(u'from@example.com', u'From Example'), 21 | to_addr=(u'to@example.com', u'To Example'), 22 | subject=u'Envelopes demo', 23 | text_body=u"I'm a helicopter!" 24 | ) 25 | envelope.add_attachment('/Users/bilbo/Pictures/helicopter.jpg') 26 | 27 | # Send the envelope using an ad-hoc connection... 28 | envelope.send('smtp.googlemail.com', login='from@example.com', 29 | password='password', tls=True) 30 | 31 | # Or send the envelope using a shared GMail connection... 32 | gmail = GMailSMTP('from@example.com', 'password') 33 | gmail.send(envelope) 34 | 35 | Features 36 | -------- 37 | 38 | Envelopes allows you to easily: 39 | 40 | * Provide e-mail addresses with or without name part. 41 | * Set text, HTML or both bodies according to your needs. 42 | * Provide any number of CC and BCC addresses. 43 | * Set standard (e.g. ``Reply-To``) and custom (e.g. ``X-Mailer``) headers. 44 | * Attach files of any kind without hassle. 45 | * Use any charset natively supported by Python's *unicode* type in addresses, 46 | bodies, headers and attachment file names. 47 | 48 | More examples 49 | ------------- 50 | 51 | .. toctree:: 52 | :maxdepth: 2 53 | 54 | examples/celery 55 | examples/flask 56 | 57 | Project status 58 | -------------- 59 | 60 | This project should be considered **beta**. Proceed with caution if you decide 61 | to use Envelopes in production. 62 | 63 | Envelopes has been developed and tested with Python 2.7. Currently, Envelopes 64 | supports Python 2.6, 2.7 and 3.3. 65 | 66 | Author 67 | ------ 68 | 69 | Envelopes is developed by `Tomasz Wójcik `_. 70 | 71 | License 72 | ------- 73 | 74 | Envelopes is licensed under the MIT License. 75 | 76 | Source code and issues 77 | ---------------------- 78 | 79 | Source code is available on GitHub at: 80 | `tomekwojcik/envelopes `_. 81 | 82 | To file issue reports and feature requests use the project's issue tracker on 83 | GitHub. 84 | 85 | API Documentation 86 | ----------------- 87 | 88 | .. toctree:: 89 | :maxdepth: 1 90 | 91 | api/envelope 92 | api/conn 93 | api/connstack 94 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Envelopes.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Envelopes.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /envelopes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | envelopes 25 | --------- 26 | 27 | Mailing for human beings. 28 | """ 29 | 30 | __version__ = '0.4' 31 | 32 | 33 | from .conn import * 34 | from .envelope import Envelope 35 | -------------------------------------------------------------------------------- /envelopes/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | import sys 24 | 25 | def encoded(_str, coding): 26 | if sys.version_info[0] == 3: 27 | return _str 28 | else: 29 | if isinstance(_str, unicode): 30 | return _str.encode(coding) 31 | else: 32 | return _str 33 | -------------------------------------------------------------------------------- /envelopes/conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | envelopes.conn 25 | ============== 26 | 27 | This module contains SMTP connection wrapper. 28 | """ 29 | 30 | import smtplib 31 | import socket 32 | 33 | TimeoutException = socket.timeout 34 | 35 | __all__ = ['SMTP', 'GMailSMTP', 'SendGridSMTP', 'MailcatcherSMTP', 36 | 'TimeoutException'] 37 | 38 | 39 | class SMTP(object): 40 | """Wrapper around :py:class:`smtplib.SMTP` class.""" 41 | 42 | def __init__(self, host=None, port=25, login=None, password=None, 43 | tls=False, timeout=None): 44 | self._conn = None 45 | self._host = host 46 | self._port = port 47 | self._login = login 48 | self._password = password 49 | self._tls = tls 50 | self._timeout = timeout 51 | 52 | @property 53 | def is_connected(self): 54 | """Returns *True* if the SMTP connection is initialized and 55 | connected. Otherwise returns *False*""" 56 | try: 57 | self._conn.noop() 58 | except (AttributeError, smtplib.SMTPServerDisconnected): 59 | return False 60 | else: 61 | return True 62 | 63 | def _connect(self, replace_current=False): 64 | if self._conn is None or replace_current: 65 | try: 66 | self._conn.quit() 67 | except (AttributeError, smtplib.SMTPServerDisconnected): 68 | pass 69 | 70 | if self._timeout: 71 | self._conn = smtplib.SMTP(self._host, self._port, 72 | timeout=self._timeout) 73 | else: 74 | self._conn = smtplib.SMTP(self._host, self._port) 75 | 76 | if self._tls: 77 | self._conn.starttls() 78 | 79 | if self._login: 80 | self._conn.login(self._login, self._password or '') 81 | 82 | def send(self, envelope): 83 | """Sends an *envelope*.""" 84 | if not self.is_connected: 85 | self._connect() 86 | 87 | msg = envelope.to_mime_message() 88 | to_addrs = [envelope._addrs_to_header([addr]) for addr in envelope._to + envelope._cc + envelope._bcc] 89 | 90 | return self._conn.sendmail(msg['From'], to_addrs, msg.as_string()) 91 | 92 | 93 | class GMailSMTP(SMTP): 94 | """Subclass of :py:class:`SMTP` preconfigured for GMail SMTP.""" 95 | 96 | GMAIL_SMTP_HOST = 'smtp.googlemail.com' 97 | GMAIL_SMTP_TLS = True 98 | 99 | def __init__(self, login=None, password=None): 100 | super(GMailSMTP, self).__init__( 101 | self.GMAIL_SMTP_HOST, tls=self.GMAIL_SMTP_TLS, login=login, 102 | password=password 103 | ) 104 | 105 | 106 | class SendGridSMTP(SMTP): 107 | """Subclass of :py:class:`SMTP` preconfigured for SendGrid SMTP.""" 108 | 109 | SENDGRID_SMTP_HOST = 'smtp.sendgrid.net' 110 | SENDGRID_SMTP_PORT = 587 111 | SENDGRID_SMTP_TLS = False 112 | 113 | def __init__(self, login=None, password=None): 114 | super(SendGridSMTP, self).__init__( 115 | self.SENDGRID_SMTP_HOST, port=self.SENDGRID_SMTP_PORT, 116 | tls=self.SENDGRID_SMTP_TLS, login=login, 117 | password=password 118 | ) 119 | 120 | 121 | class MailcatcherSMTP(SMTP): 122 | """Subclass of :py:class:`SMTP` preconfigured for local Mailcatcher 123 | SMTP.""" 124 | 125 | MAILCATCHER_SMTP_HOST = 'localhost' 126 | 127 | def __init__(self, port=1025): 128 | super(MailcatcherSMTP, self).__init__( 129 | self.MAILCATCHER_SMTP_HOST, port=port 130 | ) 131 | -------------------------------------------------------------------------------- /envelopes/connstack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2012 Vincent Driessen. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY VINCENT DRIESSEN ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 17 | # EVENT SHALL VINCENT DRIESSEN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | # The views and conclusions contained in the software and documentation are 26 | # those of the authors and should not be interpreted as representing official 27 | # policies, either expressed or implied, of Vincent Driessen. 28 | # 29 | 30 | """ 31 | envelopes.connstack 32 | =================== 33 | 34 | This module implements SMTP connection stack management. 35 | """ 36 | 37 | from contextlib import contextmanager 38 | from .local import LocalStack, release_local 39 | 40 | 41 | class NoSMTPConnectionException(Exception): 42 | pass 43 | 44 | 45 | @contextmanager 46 | def Connection(connection): 47 | push_connection(connection) 48 | try: 49 | yield 50 | finally: 51 | popped = pop_connection() 52 | assert popped == connection, \ 53 | 'Unexpected SMTP connection was popped off the stack. ' \ 54 | 'Check your SMTP connection setup.' 55 | 56 | 57 | def push_connection(connection): 58 | """Pushes the given connection on the stack.""" 59 | _connection_stack.push(connection) 60 | 61 | 62 | def pop_connection(): 63 | """Pops the topmost connection from the stack.""" 64 | return _connection_stack.pop() 65 | 66 | 67 | def use_connection(connection): 68 | """Clears the stack and uses the given connection. Protects against mixed 69 | use of use_connection() and stacked connection contexts. 70 | """ 71 | assert len(_connection_stack) <= 1, \ 72 | 'You should not mix Connection contexts with use_connection().' 73 | release_local(_connection_stack) 74 | push_connection(connection) 75 | 76 | 77 | def get_current_connection(): 78 | """Returns the current SMTP connection (i.e. the topmost on the 79 | connection stack). 80 | """ 81 | return _connection_stack.top 82 | 83 | 84 | def resolve_connection(connection=None): 85 | """Convenience function to resolve the given or the current connection. 86 | Raises an exception if it cannot resolve a connection now. 87 | """ 88 | if connection is not None: 89 | return connection 90 | 91 | connection = get_current_connection() 92 | if connection is None: 93 | raise NoSMTPConnectionException( 94 | 'Could not resolve an SMTP connection.') 95 | return connection 96 | 97 | 98 | _connection_stack = LocalStack() 99 | 100 | __all__ = [ 101 | 'Connection', 'get_current_connection', 'push_connection', 102 | 'pop_connection', 'use_connection' 103 | ] 104 | -------------------------------------------------------------------------------- /envelopes/envelope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | envelopes.envelope 25 | ================== 26 | 27 | This module contains the Envelope class. 28 | """ 29 | 30 | import sys 31 | 32 | if sys.version_info[0] == 2: 33 | from email import Encoders as email_encoders 34 | elif sys.version_info[0] == 3: 35 | from email import encoders as email_encoders 36 | basestring = str 37 | 38 | def unicode(_str, _charset): 39 | return str(_str.encode(_charset), _charset) 40 | else: 41 | raise RuntimeError('Unsupported Python version: %d.%d.%d' % ( 42 | sys.version_info[0], sys.version_info[1], sys.version_info[2] 43 | )) 44 | 45 | from email.header import Header 46 | from email.mime.base import MIMEBase 47 | from email.mime.multipart import MIMEMultipart 48 | from email.mime.application import MIMEApplication 49 | from email.mime.audio import MIMEAudio 50 | from email.mime.image import MIMEImage 51 | from email.mime.text import MIMEText 52 | import mimetypes 53 | import os 54 | import re 55 | 56 | from .conn import SMTP 57 | from .compat import encoded 58 | 59 | 60 | class MessageEncodeError(Exception): 61 | pass 62 | 63 | class Envelope(object): 64 | """ 65 | The Envelope class. 66 | 67 | **Address formats** 68 | 69 | The following formats are supported for e-mail addresses: 70 | 71 | * ``"user@server.com"`` - just the e-mail address part as a string, 72 | * ``"Some User "`` - name and e-mail address parts as a string, 73 | * ``("user@server.com", "Some User")`` - e-mail address and name parts as a tuple. 74 | 75 | Whenever you come to manipulate addresses feel free to use any (or all) of 76 | the formats above. 77 | 78 | :param to_addr: ``To`` address or list of ``To`` addresses 79 | :param from_addr: ``From`` address 80 | :param subject: message subject 81 | :param html_body: optional HTML part of the message 82 | :param text_body: optional plain text part of the message 83 | :param cc_addr: optional single CC address or list of CC addresses 84 | :param bcc_addr: optional single BCC address or list of BCC addresses 85 | :param headers: optional dictionary of headers 86 | :param charset: message charset 87 | """ 88 | 89 | ADDR_FORMAT = '%s <%s>' 90 | ADDR_REGEXP = re.compile(r'^(.*) <([^@]+@[^@]+)>$') 91 | 92 | def __init__(self, to_addr=None, from_addr=None, subject=None, 93 | html_body=None, text_body=None, cc_addr=None, bcc_addr=None, 94 | headers=None, charset='utf-8'): 95 | if to_addr: 96 | if isinstance(to_addr, list): 97 | self._to = to_addr 98 | else: 99 | self._to = [to_addr] 100 | else: 101 | self._to = [] 102 | 103 | self._from = from_addr 104 | self._subject = subject 105 | self._parts = [] 106 | 107 | if text_body: 108 | self._parts.append(('text/plain', text_body, charset)) 109 | 110 | if html_body: 111 | self._parts.append(('text/html', html_body, charset)) 112 | 113 | if cc_addr: 114 | if isinstance(cc_addr, list): 115 | self._cc = cc_addr 116 | else: 117 | self._cc = [cc_addr] 118 | else: 119 | self._cc = [] 120 | 121 | if bcc_addr: 122 | if isinstance(bcc_addr, list): 123 | self._bcc = bcc_addr 124 | else: 125 | self._bcc = [bcc_addr] 126 | else: 127 | self._bcc = [] 128 | 129 | if headers: 130 | self._headers = headers 131 | else: 132 | self._headers = {} 133 | 134 | self._charset = charset 135 | 136 | self._addr_format = unicode(self.ADDR_FORMAT, charset) 137 | 138 | def __repr__(self): 139 | return u'' % ( 140 | self._addrs_to_header([self._from]), 141 | self._addrs_to_header(self._to), 142 | self._subject 143 | ) 144 | 145 | @property 146 | def to_addr(self): 147 | """List of ``To`` addresses.""" 148 | return self._to 149 | 150 | def add_to_addr(self, to_addr): 151 | """Adds a ``To`` address.""" 152 | self._to.append(to_addr) 153 | 154 | def clear_to_addr(self): 155 | """Clears list of ``To`` addresses.""" 156 | self._to = [] 157 | 158 | @property 159 | def from_addr(self): 160 | return self._from 161 | 162 | @from_addr.setter 163 | def from_addr(self, from_addr): 164 | self._from = from_addr 165 | 166 | @property 167 | def cc_addr(self): 168 | """List of CC addresses.""" 169 | return self._cc 170 | 171 | def add_cc_addr(self, cc_addr): 172 | """Adds a CC address.""" 173 | self._cc.append(cc_addr) 174 | 175 | def clear_cc_addr(self): 176 | """Clears list of CC addresses.""" 177 | self._cc = [] 178 | 179 | @property 180 | def bcc_addr(self): 181 | """List of BCC addresses.""" 182 | return self._bcc 183 | 184 | def add_bcc_addr(self, bcc_addr): 185 | """Adds a BCC address.""" 186 | self._bcc.append(bcc_addr) 187 | 188 | def clear_bcc_addr(self): 189 | """Clears list of BCC addresses.""" 190 | self._bcc = [] 191 | 192 | @property 193 | def charset(self): 194 | """Message charset.""" 195 | return self._charset 196 | 197 | @charset.setter 198 | def charset(self, charset): 199 | self._charset = charset 200 | 201 | self._addr_format = unicode(self.ADDR_FORMAT, charset) 202 | 203 | def _addr_tuple_to_addr(self, addr_tuple): 204 | addr = '' 205 | 206 | if len(addr_tuple) == 2 and addr_tuple[1]: 207 | addr = self._addr_format % ( 208 | self._header(addr_tuple[1] or ''), 209 | addr_tuple[0] or '' 210 | ) 211 | elif addr_tuple[0]: 212 | addr = addr_tuple[0] 213 | 214 | return addr 215 | 216 | @property 217 | def headers(self): 218 | """Dictionary of custom headers.""" 219 | return self._headers 220 | 221 | def add_header(self, key, value): 222 | """Adds a custom header.""" 223 | self._headers[key] = value 224 | 225 | def clear_headers(self): 226 | """Clears custom headers.""" 227 | self._headers = {} 228 | 229 | def _addrs_to_header(self, addrs): 230 | _addrs = [] 231 | for addr in addrs: 232 | if not addr: 233 | continue 234 | 235 | if isinstance(addr, basestring): 236 | if self._is_ascii(addr): 237 | _addrs.append(self._encoded(addr)) 238 | else: 239 | # these headers need special care when encoding, see: 240 | # http://tools.ietf.org/html/rfc2047#section-8 241 | # Need to break apart the name from the address if there are 242 | # non-ascii chars 243 | m = self.ADDR_REGEXP.match(addr) 244 | if m: 245 | t = (m.group(2), m.group(1)) 246 | _addrs.append(self._addr_tuple_to_addr(t)) 247 | else: 248 | # What can we do? Just pass along what the user gave us and hope they did it right 249 | _addrs.append(self._encoded(addr)) 250 | elif isinstance(addr, tuple): 251 | _addrs.append(self._addr_tuple_to_addr(addr)) 252 | else: 253 | self._raise(MessageEncodeError, 254 | '%s is not a valid address' % str(addr)) 255 | 256 | _header = ','.join(_addrs) 257 | return _header 258 | 259 | def _raise(self, exc_class, message): 260 | raise exc_class(self._encoded(message)) 261 | 262 | def _header(self, _str): 263 | if self._is_ascii(_str): 264 | return _str 265 | return Header(_str, self._charset).encode() 266 | 267 | def _is_ascii(self, _str): 268 | return all(ord(c) < 128 for c in _str) 269 | 270 | def _encoded(self, _str): 271 | return encoded(_str, self._charset) 272 | 273 | def to_mime_message(self): 274 | """Returns the envelope as 275 | :py:class:`email.mime.multipart.MIMEMultipart`.""" 276 | msg = MIMEMultipart('alternative') 277 | msg['Subject'] = self._header(self._subject or '') 278 | 279 | msg['From'] = self._encoded(self._addrs_to_header([self._from])) 280 | msg['To'] = self._encoded(self._addrs_to_header(self._to)) 281 | 282 | if self._cc: 283 | msg['CC'] = self._addrs_to_header(self._cc) 284 | 285 | if self._headers: 286 | for key, value in self._headers.items(): 287 | msg[key] = self._header(value) 288 | 289 | for part in self._parts: 290 | type_maj, type_min = part[0].split('/') 291 | if type_maj == 'text' and type_min in ('html', 'plain'): 292 | msg.attach(MIMEText(part[1], type_min, self._charset)) 293 | else: 294 | msg.attach(part[1]) 295 | 296 | return msg 297 | 298 | def add_attachment(self, file_path, mimetype=None): 299 | """Attaches a file located at *file_path* to the envelope. If 300 | *mimetype* is not specified an attempt to guess it is made. If nothing 301 | is guessed then `application/octet-stream` is used.""" 302 | if not mimetype: 303 | mimetype, _ = mimetypes.guess_type(file_path) 304 | 305 | if mimetype is None: 306 | mimetype = 'application/octet-stream' 307 | 308 | type_maj, type_min = mimetype.split('/') 309 | with open(file_path, 'rb') as fh: 310 | part_data = fh.read() 311 | 312 | part = MIMEBase(type_maj, type_min) 313 | part.set_payload(part_data) 314 | email_encoders.encode_base64(part) 315 | 316 | part_filename = os.path.basename(self._encoded(file_path)) 317 | part.add_header('Content-Disposition', 'attachment; filename="%s"' 318 | % part_filename) 319 | 320 | self._parts.append((mimetype, part)) 321 | 322 | def send(self, *args, **kwargs): 323 | """Sends the envelope using a freshly created SMTP connection. *args* 324 | and *kwargs* are passed directly to :py:class:`envelopes.conn.SMTP` 325 | constructor. 326 | 327 | Returns a tuple of SMTP object and whatever its send method returns.""" 328 | conn = SMTP(*args, **kwargs) 329 | send_result = conn.send(self) 330 | return conn, send_result 331 | -------------------------------------------------------------------------------- /envelopes/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2011 by the Werkzeug Team, see AUTHORS for more details. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 11 | # * Redistributions in binary form must reproduce the above 12 | # copyright notice, this list of conditions and the following 13 | # disclaimer in the documentation and/or other materials provided 14 | # with the distribution. 15 | # 16 | # * The names of the contributors may not be used to endorse or 17 | # promote products derived from this software without specific 18 | # prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | # 32 | 33 | """ 34 | werkzeug.local 35 | ~~~~~~~~~~~~~~ 36 | 37 | This module implements context-local objects. 38 | 39 | :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. 40 | :license: BSD, see LICENSE for more details. 41 | """ 42 | # Since each thread has its own greenlet we can just use those as identifiers 43 | # for the context. If greenlets are not available we fall back to the 44 | # current thread ident. 45 | try: 46 | from greenlet import getcurrent as get_ident 47 | except ImportError: # noqa 48 | try: 49 | from thread import get_ident # noqa 50 | except ImportError: # noqa 51 | from _thread import get_ident # noqa 52 | 53 | 54 | def release_local(local): 55 | """Releases the contents of the local for the current context. 56 | This makes it possible to use locals without a manager. 57 | 58 | Example:: 59 | 60 | >>> loc = Local() 61 | >>> loc.foo = 42 62 | >>> release_local(loc) 63 | >>> hasattr(loc, 'foo') 64 | False 65 | 66 | With this function one can release :class:`Local` objects as well 67 | as :class:`StackLocal` objects. However it is not possible to 68 | release data held by proxies that way, one always has to retain 69 | a reference to the underlying local object in order to be able 70 | to release it. 71 | 72 | .. versionadded:: 0.6.1 73 | """ 74 | local.__release_local__() 75 | 76 | 77 | class Local(object): 78 | __slots__ = ('__storage__', '__ident_func__') 79 | 80 | def __init__(self): 81 | object.__setattr__(self, '__storage__', {}) 82 | object.__setattr__(self, '__ident_func__', get_ident) 83 | 84 | def __iter__(self): 85 | return iter(self.__storage__.items()) 86 | 87 | def __call__(self, proxy): 88 | """Create a proxy for a name.""" 89 | return LocalProxy(self, proxy) 90 | 91 | def __release_local__(self): 92 | self.__storage__.pop(self.__ident_func__(), None) 93 | 94 | def __getattr__(self, name): 95 | try: 96 | return self.__storage__[self.__ident_func__()][name] 97 | except KeyError: 98 | raise AttributeError(name) 99 | 100 | def __setattr__(self, name, value): 101 | ident = self.__ident_func__() 102 | storage = self.__storage__ 103 | try: 104 | storage[ident][name] = value 105 | except KeyError: 106 | storage[ident] = {name: value} 107 | 108 | def __delattr__(self, name): 109 | try: 110 | del self.__storage__[self.__ident_func__()][name] 111 | except KeyError: 112 | raise AttributeError(name) 113 | 114 | 115 | class LocalStack(object): 116 | """This class works similar to a :class:`Local` but keeps a stack 117 | of objects instead. This is best explained with an example:: 118 | 119 | >>> ls = LocalStack() 120 | >>> ls.push(42) 121 | >>> ls.top 122 | 42 123 | >>> ls.push(23) 124 | >>> ls.top 125 | 23 126 | >>> ls.pop() 127 | 23 128 | >>> ls.top 129 | 42 130 | 131 | They can be force released by using a :class:`LocalManager` or with 132 | the :func:`release_local` function but the correct way is to pop the 133 | item from the stack after using. When the stack is empty it will 134 | no longer be bound to the current context (and as such released). 135 | 136 | By calling the stack without arguments it returns a proxy that resolves to 137 | the topmost item on the stack. 138 | 139 | .. versionadded:: 0.6.1 140 | """ 141 | 142 | def __init__(self): 143 | self._local = Local() 144 | 145 | def __release_local__(self): 146 | self._local.__release_local__() 147 | 148 | def _get__ident_func__(self): 149 | return self._local.__ident_func__ 150 | 151 | def _set__ident_func__(self, value): # noqa 152 | object.__setattr__(self._local, '__ident_func__', value) 153 | __ident_func__ = property(_get__ident_func__, _set__ident_func__) 154 | del _get__ident_func__, _set__ident_func__ 155 | 156 | def __call__(self): 157 | def _lookup(): 158 | rv = self.top 159 | if rv is None: 160 | raise RuntimeError('object unbound') 161 | return rv 162 | return LocalProxy(_lookup) 163 | 164 | def push(self, obj): 165 | """Pushes a new item to the stack""" 166 | rv = getattr(self._local, 'stack', None) 167 | if rv is None: 168 | self._local.stack = rv = [] 169 | rv.append(obj) 170 | return rv 171 | 172 | def pop(self): 173 | """Removes the topmost item from the stack, will return the 174 | old value or `None` if the stack was already empty. 175 | """ 176 | stack = getattr(self._local, 'stack', None) 177 | if stack is None: 178 | return None 179 | elif len(stack) == 1: 180 | release_local(self._local) 181 | return stack[-1] 182 | else: 183 | return stack.pop() 184 | 185 | @property 186 | def top(self): 187 | """The topmost item on the stack. If the stack is empty, 188 | `None` is returned. 189 | """ 190 | try: 191 | return self._local.stack[-1] 192 | except (AttributeError, IndexError): 193 | return None 194 | 195 | def __len__(self): 196 | stack = getattr(self._local, 'stack', None) 197 | if stack is None: 198 | return 0 199 | return len(stack) 200 | 201 | 202 | class LocalManager(object): 203 | """Local objects cannot manage themselves. For that you need a local 204 | manager. You can pass a local manager multiple locals or add them later 205 | by appending them to `manager.locals`. Everytime the manager cleans up 206 | it, will clean up all the data left in the locals for this context. 207 | 208 | The `ident_func` parameter can be added to override the default ident 209 | function for the wrapped locals. 210 | 211 | .. versionchanged:: 0.6.1 212 | Instead of a manager the :func:`release_local` function can be used 213 | as well. 214 | 215 | .. versionchanged:: 0.7 216 | `ident_func` was added. 217 | """ 218 | 219 | def __init__(self, locals=None, ident_func=None): 220 | if locals is None: 221 | self.locals = [] 222 | elif isinstance(locals, Local): 223 | self.locals = [locals] 224 | else: 225 | self.locals = list(locals) 226 | if ident_func is not None: 227 | self.ident_func = ident_func 228 | for local in self.locals: 229 | object.__setattr__(local, '__ident_func__', ident_func) 230 | else: 231 | self.ident_func = get_ident 232 | 233 | def get_ident(self): 234 | """Return the context identifier the local objects use internally for 235 | this context. You cannot override this method to change the behavior 236 | but use it to link other context local objects (such as SQLAlchemy's 237 | scoped sessions) to the Werkzeug locals. 238 | 239 | .. versionchanged:: 0.7 240 | Yu can pass a different ident function to the local manager that 241 | will then be propagated to all the locals passed to the 242 | constructor. 243 | """ 244 | return self.ident_func() 245 | 246 | def cleanup(self): 247 | """Manually clean up the data in the locals for this context. Call 248 | this at the end of the request or use `make_middleware()`. 249 | """ 250 | for local in self.locals: 251 | release_local(local) 252 | 253 | def __repr__(self): 254 | return '<%s storages: %d>' % ( 255 | self.__class__.__name__, 256 | len(self.locals) 257 | ) 258 | 259 | 260 | class LocalProxy(object): 261 | """Acts as a proxy for a werkzeug local. Forwards all operations to 262 | a proxied object. The only operations not supported for forwarding 263 | are right handed operands and any kind of assignment. 264 | 265 | Example usage:: 266 | 267 | from werkzeug.local import Local 268 | l = Local() 269 | 270 | # these are proxies 271 | request = l('request') 272 | user = l('user') 273 | 274 | 275 | from werkzeug.local import LocalStack 276 | _response_local = LocalStack() 277 | 278 | # this is a proxy 279 | response = _response_local() 280 | 281 | Whenever something is bound to l.user / l.request the proxy objects 282 | will forward all operations. If no object is bound a :exc:`RuntimeError` 283 | will be raised. 284 | 285 | To create proxies to :class:`Local` or :class:`LocalStack` objects, 286 | call the object as shown above. If you want to have a proxy to an 287 | object looked up by a function, you can (as of Werkzeug 0.6.1) pass 288 | a function to the :class:`LocalProxy` constructor:: 289 | 290 | session = LocalProxy(lambda: get_current_request().session) 291 | 292 | .. versionchanged:: 0.6.1 293 | The class can be instanciated with a callable as well now. 294 | """ 295 | __slots__ = ('__local', '__dict__', '__name__') 296 | 297 | def __init__(self, local, name=None): 298 | object.__setattr__(self, '_LocalProxy__local', local) 299 | object.__setattr__(self, '__name__', name) 300 | 301 | def _get_current_object(self): 302 | """Return the current object. This is useful if you want the real 303 | object behind the proxy at a time for performance reasons or because 304 | you want to pass the object into a different context. 305 | """ 306 | if not hasattr(self.__local, '__release_local__'): 307 | return self.__local() 308 | try: 309 | return getattr(self.__local, self.__name__) 310 | except AttributeError: 311 | raise RuntimeError('no object bound to %s' % self.__name__) 312 | 313 | @property 314 | def __dict__(self): 315 | try: 316 | return self._get_current_object().__dict__ 317 | except RuntimeError: 318 | raise AttributeError('__dict__') 319 | 320 | def __repr__(self): 321 | try: 322 | obj = self._get_current_object() 323 | except RuntimeError: 324 | return '<%s unbound>' % self.__class__.__name__ 325 | return repr(obj) 326 | 327 | def __nonzero__(self): 328 | try: 329 | return bool(self._get_current_object()) 330 | except RuntimeError: 331 | return False 332 | 333 | def __unicode__(self): 334 | try: 335 | return unicode(self._get_current_object()) 336 | except RuntimeError: 337 | return repr(self) 338 | 339 | def __dir__(self): 340 | try: 341 | return dir(self._get_current_object()) 342 | except RuntimeError: 343 | return [] 344 | 345 | def __getattr__(self, name): 346 | if name == '__members__': 347 | return dir(self._get_current_object()) 348 | return getattr(self._get_current_object(), name) 349 | 350 | def __setitem__(self, key, value): 351 | self._get_current_object()[key] = value 352 | 353 | def __delitem__(self, key): 354 | del self._get_current_object()[key] 355 | 356 | def __setslice__(self, i, j, seq): 357 | self._get_current_object()[i:j] = seq 358 | 359 | def __delslice__(self, i, j): 360 | del self._get_current_object()[i:j] 361 | 362 | __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v) 363 | __delattr__ = lambda x, n: delattr(x._get_current_object(), n) 364 | __str__ = lambda x: str(x._get_current_object()) 365 | __lt__ = lambda x, o: x._get_current_object() < o 366 | __le__ = lambda x, o: x._get_current_object() <= o 367 | __eq__ = lambda x, o: x._get_current_object() == o 368 | __ne__ = lambda x, o: x._get_current_object() != o 369 | __gt__ = lambda x, o: x._get_current_object() > o 370 | __ge__ = lambda x, o: x._get_current_object() >= o 371 | __cmp__ = lambda x, o: cmp(x._get_current_object(), o) 372 | __hash__ = lambda x: hash(x._get_current_object()) 373 | __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw) 374 | __len__ = lambda x: len(x._get_current_object()) 375 | __getitem__ = lambda x, i: x._get_current_object()[i] 376 | __iter__ = lambda x: iter(x._get_current_object()) 377 | __contains__ = lambda x, i: i in x._get_current_object() 378 | __getslice__ = lambda x, i, j: x._get_current_object()[i:j] 379 | __add__ = lambda x, o: x._get_current_object() + o 380 | __sub__ = lambda x, o: x._get_current_object() - o 381 | __mul__ = lambda x, o: x._get_current_object() * o 382 | __floordiv__ = lambda x, o: x._get_current_object() // o 383 | __mod__ = lambda x, o: x._get_current_object() % o 384 | __divmod__ = lambda x, o: x._get_current_object().__divmod__(o) 385 | __pow__ = lambda x, o: x._get_current_object() ** o 386 | __lshift__ = lambda x, o: x._get_current_object() << o 387 | __rshift__ = lambda x, o: x._get_current_object() >> o 388 | __and__ = lambda x, o: x._get_current_object() & o 389 | __xor__ = lambda x, o: x._get_current_object() ^ o 390 | __or__ = lambda x, o: x._get_current_object() | o 391 | __div__ = lambda x, o: x._get_current_object().__div__(o) 392 | __truediv__ = lambda x, o: x._get_current_object().__truediv__(o) 393 | __neg__ = lambda x: -(x._get_current_object()) 394 | __pos__ = lambda x: +(x._get_current_object()) 395 | __abs__ = lambda x: abs(x._get_current_object()) 396 | __invert__ = lambda x: ~(x._get_current_object()) 397 | __complex__ = lambda x: complex(x._get_current_object()) 398 | __int__ = lambda x: int(x._get_current_object()) 399 | __long__ = lambda x: long(x._get_current_object()) 400 | __float__ = lambda x: float(x._get_current_object()) 401 | __oct__ = lambda x: oct(x._get_current_object()) 402 | __hex__ = lambda x: hex(x._get_current_object()) 403 | __index__ = lambda x: x._get_current_object().__index__() 404 | __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o) 405 | __enter__ = lambda x: x._get_current_object().__enter__() 406 | __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw) 407 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomekwojcik/envelopes/8ad190a55d0d8b805b6ae545b896e719467253b7/examples/__init__.py -------------------------------------------------------------------------------- /examples/example_celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | import os 24 | import sys 25 | sys.path = ['.'] + sys.path 26 | 27 | from celery import Celery 28 | from envelopes import Envelope 29 | 30 | celery = Celery('envelopes_demo') 31 | celery.conf.BROKER_URL = 'amqp://guest@localhost//' 32 | 33 | 34 | @celery.task 35 | def send_envelope(): 36 | envelope = Envelope( 37 | from_addr='%s@localhost' % os.getlogin(), 38 | to_addr='%s@localhost' % os.getlogin(), 39 | subject='Envelopes in Celery demo', 40 | text_body="I'm a helicopter!" 41 | ) 42 | envelope.send('localhost', port=1025) 43 | -------------------------------------------------------------------------------- /examples/example_flask.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | import sys 24 | sys.path = ['.'] + sys.path 25 | 26 | from envelopes import Envelope, SMTP 27 | import envelopes.connstack 28 | from flask import Flask, jsonify 29 | import os 30 | 31 | 32 | app = Flask(__name__) 33 | app.config['DEBUG'] = True 34 | 35 | conn = SMTP('127.0.0.1', 1025) 36 | 37 | 38 | @app.before_request 39 | def app_before_request(): 40 | envelopes.connstack.push_connection(conn) 41 | 42 | 43 | @app.after_request 44 | def app_after_request(response): 45 | envelopes.connstack.pop_connection() 46 | return response 47 | 48 | 49 | @app.route('/mail', methods=['POST']) 50 | def post_mail(): 51 | envelope = Envelope( 52 | from_addr='%s@localhost' % os.getlogin(), 53 | to_addr='%s@localhost' % os.getlogin(), 54 | subject='Envelopes in Flask demo', 55 | text_body="I'm a helicopter!" 56 | ) 57 | 58 | smtp = envelopes.connstack.get_current_connection() 59 | smtp.send(envelope) 60 | 61 | return jsonify(dict(status='ok')) 62 | 63 | if __name__ == '__main__': 64 | app.run() 65 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomekwojcik/envelopes/8ad190a55d0d8b805b6ae545b896e719467253b7/lib/__init__.py -------------------------------------------------------------------------------- /lib/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | lib.testing 25 | =========== 26 | 27 | Various utilities used in testing. 28 | """ 29 | 30 | import codecs 31 | import json 32 | import os 33 | import smtplib 34 | import tempfile 35 | 36 | HTML_BODY = u""" 37 | 38 |

I'm a helicopter!

39 | """ 40 | 41 | TEXT_BODY = u"""I'm a helicopter!""" 42 | 43 | 44 | class MockSMTP(object): 45 | """A class that mocks ``smtp.SMTP``.""" 46 | 47 | debuglevel = 0 48 | file = None 49 | helo_resp = None 50 | ehlo_msg = "ehlo" 51 | ehlo_resp = None 52 | does_esmtp = 0 53 | default_port = smtplib.SMTP_PORT 54 | 55 | def __init__(self, host='', port=0, local_hostname=None, timeout=0): 56 | self._host = host 57 | self._port = port 58 | self._local_hostname = local_hostname 59 | self._timeout = timeout 60 | self._call_stack = {} 61 | 62 | def __append_call(self, method, args, kwargs): 63 | if method not in self._call_stack: 64 | self._call_stack[method] = [] 65 | 66 | self._call_stack[method].append((args, kwargs)) 67 | 68 | def set_debuglevel(self, debuglevel): 69 | self.debuglevel = debuglevel 70 | 71 | def _get_socket(self, port, host, timeout): 72 | return None 73 | 74 | def connect(self, host='localhost', port=0): 75 | self.__append_call('connect', [], dict(host=host, port=port)) 76 | 77 | def send(self, str): 78 | self.__append_call('connect', [str], {}) 79 | 80 | def putcmd(self, cmd, args=""): 81 | self.__append_call('connect', [cmd], dict(args=args)) 82 | 83 | def getreply(self): 84 | self.__append_call('getreply', [], dict()) 85 | 86 | def docmd(self, cmd, args=""): 87 | self.__append_call('docmd', [cmd], dict(args=args)) 88 | 89 | def helo(self, name=''): 90 | self.__append_call('helo', [], dict(name=name)) 91 | 92 | def ehlo(self, name=''): 93 | self.__append_call('ehlo', [], dict(name=name)) 94 | 95 | def has_extn(self, opt): 96 | self.__append_call('has_extn', [opt], dict()) 97 | 98 | def help(self, args=''): 99 | self.__append_call('help', [], dict(args=args)) 100 | 101 | def rset(self): 102 | self.__append_call('rset', [], dict()) 103 | 104 | def noop(self): 105 | self.__append_call('noop', [], dict()) 106 | 107 | def mail(self, sender, options=[]): 108 | self.__append_call('mail', [sender], dict(options=options)) 109 | 110 | def rcpt(self, recip, options=[]): 111 | self.__append_call('rcpt', [recip], dict(options=options)) 112 | 113 | def data(self, msg): 114 | self.__append_call('data', [msg], dict()) 115 | 116 | def verify(self, address): 117 | self.__append_call('verify', [address], dict()) 118 | 119 | vrfy = verify 120 | 121 | def expn(self, address): 122 | self.__append_call('expn', [address], dict()) 123 | 124 | def ehlo_or_helo_if_needed(self): 125 | self.__append_call('ehlo_or_helo_if_needed', [], dict()) 126 | 127 | def login(self, user, password): 128 | self.__append_call('login', [user, password], dict()) 129 | 130 | def starttls(self, keyfile=None, certfile=None): 131 | self.__append_call('starttls', [], dict(keyfile=keyfile, 132 | certfile=certfile)) 133 | 134 | def sendmail(self, from_addr, to_addrs, msg, mail_options=[], 135 | rcpt_options=[]): 136 | _args = [from_addr, to_addrs, msg] 137 | _kwargs = dict(mail_options=mail_options, rcpt_options=rcpt_options) 138 | self.__append_call('sendmail', _args, _kwargs) 139 | 140 | def close(self): 141 | self.__append_call('close', [], dict()) 142 | 143 | def quit(self): 144 | self.__append_call('quit', [], dict()) 145 | 146 | 147 | class BaseTestCase(object): 148 | """Base class for Envelopes test cases.""" 149 | 150 | @classmethod 151 | def setUpClass(cls): 152 | cls._tempfiles = [] 153 | 154 | @classmethod 155 | def tearDownClass(cls): 156 | for tempfile in cls._tempfiles: 157 | os.unlink(tempfile) 158 | 159 | def tearDown(self): 160 | self._unpatch_smtplib() 161 | 162 | def _patch_smtplib(self): 163 | self._orig_smtp = smtplib.SMTP 164 | smtplib.SMTP = MockSMTP 165 | 166 | def _unpatch_smtplib(self): 167 | if hasattr(self, '_orig_smtp'): 168 | smtplib.SMTP = self._orig_smtp 169 | 170 | def _dummy_message(self): 171 | return dict({ 172 | 'to_addr': ('to@example.com', 'Example To'), 173 | 'from_addr': ('from@example.com', 'Example From'), 174 | 'subject': "I'm a helicopter!", 175 | 'html_body': HTML_BODY, 176 | 'text_body': TEXT_BODY, 177 | 'cc_addr': [ 178 | 'cc1@example.com', 179 | 'Example CC2 ', 180 | ('cc3@example.com', 'Example CC3') 181 | ], 182 | 'bcc_addr': [ 183 | 'bcc1@example.com', 184 | 'Example BCC2 ', 185 | ('bcc3@example.com', 'Example BCC3') 186 | ], 187 | 'headers': { 188 | 'Reply-To': 'reply-to@example.com', 189 | 'X-Mailer': 'Envelopes by BTHLabs' 190 | }, 191 | 'charset': 'utf-8' 192 | }) 193 | 194 | def _tempfile(self, **kwargs): 195 | fd, path = tempfile.mkstemp(**kwargs) 196 | os.close(fd) 197 | self._tempfiles.append(path) 198 | 199 | return path 200 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2013 Tomasz Wójcik 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | # 23 | 24 | import codecs 25 | from setuptools import setup 26 | 27 | import envelopes 28 | 29 | desc_file = codecs.open('README.rst', 'r', 'utf-8') 30 | long_description = desc_file.read() 31 | desc_file.close() 32 | 33 | setup( 34 | name="Envelopes", 35 | version=envelopes.__version__, 36 | packages=['envelopes'], 37 | test_suite='nose.collector', 38 | zip_safe=False, 39 | platforms='any', 40 | tests_require=[ 41 | 'nose', 42 | ], 43 | author=u'Tomasz Wójcik'.encode('utf-8'), 44 | author_email='tomek@bthlabs.pl', 45 | maintainer=u'Tomasz Wójcik'.encode('utf-8'), 46 | maintainer_email='tomek@bthlabs.pl', 47 | url='http://tomekwojcik.github.io/envelopes/', 48 | download_url='http://github.com/tomekwojcik/envelopes/tarball/v%s' %\ 49 | envelopes.__version__, 50 | description='Mailing for human beings', 51 | long_description=long_description, 52 | license='https://github.com/tomekwojcik/envelopes/blob/master/LICENSE', 53 | classifiers=[ 54 | "Development Status :: 4 - Beta", 55 | "Environment :: Other Environment", 56 | "Intended Audience :: Developers", 57 | "License :: OSI Approved :: MIT License", 58 | "Operating System :: OS Independent", 59 | "Programming Language :: Python :: 2", 60 | "Programming Language :: Python :: 2.6", 61 | "Programming Language :: Python :: 2.7", 62 | "Programming Language :: Python :: 3", 63 | "Programming Language :: Python :: 3.3", 64 | "Topic :: Software Development :: Libraries :: Python Modules" 65 | ] 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | test_conn 25 | ========= 26 | 27 | This module contains test suite for the *SMTP* class. 28 | """ 29 | 30 | from envelopes.conn import SMTP 31 | from envelopes.envelope import Envelope 32 | from lib.testing import BaseTestCase 33 | 34 | 35 | class Test_SMTPConnection(BaseTestCase): 36 | def setUp(self): 37 | self._patch_smtplib() 38 | 39 | def test_constructor(self): 40 | conn = SMTP('localhost', port=587, login='spam', 41 | password='eggs', tls=True, timeout=10) 42 | 43 | assert conn._conn is None 44 | assert conn._host == 'localhost' 45 | assert conn._port == 587 46 | assert conn._login == 'spam' 47 | assert conn._password == 'eggs' 48 | assert conn._tls is True 49 | assert conn._timeout == 10 50 | 51 | def test_constructor_all_kwargs(self): 52 | conn = SMTP(host='localhost', port=587, login='spam', 53 | password='eggs', tls=True) 54 | 55 | assert conn._conn is None 56 | assert conn._host == 'localhost' 57 | assert conn._port == 587 58 | assert conn._login == 'spam' 59 | assert conn._password == 'eggs' 60 | assert conn._tls is True 61 | 62 | def test_connect(self): 63 | conn = SMTP('localhost') 64 | conn._connect() 65 | assert conn._conn is not None 66 | 67 | old_conn = conn._conn 68 | conn._connect() 69 | assert old_conn == conn._conn 70 | 71 | def test_connect_replace_current(self): 72 | conn = SMTP('localhost') 73 | conn._connect() 74 | assert conn._conn is not None 75 | 76 | old_conn = conn._conn 77 | conn._connect(replace_current=True) 78 | assert conn._conn is not None 79 | assert conn._conn != old_conn 80 | 81 | def test_connect_starttls(self): 82 | conn = SMTP('localhost', tls=False) 83 | conn._connect() 84 | assert conn._conn is not None 85 | assert len(conn._conn._call_stack.get('starttls', [])) == 0 86 | 87 | conn = SMTP('localhost', tls=True) 88 | conn._connect() 89 | assert conn._conn is not None 90 | assert len(conn._conn._call_stack.get('starttls', [])) == 1 91 | 92 | def test_connect_login(self): 93 | conn = SMTP('localhost') 94 | conn._connect() 95 | assert conn._conn is not None 96 | assert len(conn._conn._call_stack.get('login', [])) == 0 97 | 98 | conn = SMTP('localhost', login='spam') 99 | conn._connect() 100 | assert conn._conn is not None 101 | assert len(conn._conn._call_stack.get('login', [])) == 1 102 | 103 | call_args = conn._conn._call_stack['login'][0][0] 104 | assert len(call_args) == 2 105 | assert call_args[0] == conn._login 106 | assert call_args[1] == '' 107 | 108 | conn = SMTP('localhost', login='spam', password='eggs') 109 | conn._connect() 110 | assert conn._conn is not None 111 | assert len(conn._conn._call_stack.get('login', [])) == 1 112 | 113 | call_args = conn._conn._call_stack['login'][0][0] 114 | assert len(call_args) == 2 115 | assert call_args[0] == conn._login 116 | assert call_args[1] == conn._password 117 | 118 | def test_is_connected(self): 119 | conn = SMTP('localhost') 120 | assert conn.is_connected is False 121 | 122 | conn._connect() 123 | assert conn.is_connected is True 124 | assert len(conn._conn._call_stack.get('noop', [])) == 1 125 | 126 | def test_send(self): 127 | conn = SMTP('localhost') 128 | 129 | msg = self._dummy_message() 130 | envelope = Envelope(**msg) 131 | mime_msg = envelope.to_mime_message() 132 | 133 | conn.send(envelope) 134 | assert conn._conn is not None 135 | assert len(conn._conn._call_stack.get('sendmail', [])) == 1 136 | 137 | call_args = conn._conn._call_stack['sendmail'][0][0] 138 | assert len(call_args) == 3 139 | assert call_args[0] == mime_msg['From'] 140 | assert call_args[1] == [envelope._addrs_to_header([addr]) for addr in envelope._to + envelope._cc + envelope._bcc] 141 | assert call_args[2] != '' 142 | -------------------------------------------------------------------------------- /tests/test_envelope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013 Tomasz Wójcik 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | """ 24 | test_envelope 25 | ============= 26 | 27 | This module contains test suite for the *Envelope* class. 28 | """ 29 | 30 | from email.header import Header 31 | import os 32 | import sys 33 | 34 | from envelopes.envelope import Envelope, MessageEncodeError 35 | from envelopes.compat import encoded 36 | from lib.testing import BaseTestCase 37 | 38 | 39 | class Test_Envelope(BaseTestCase): 40 | def setUp(self): 41 | self._patch_smtplib() 42 | 43 | def test_constructor(self): 44 | msg = self._dummy_message() 45 | envelope = Envelope(**msg) 46 | 47 | assert envelope._to == [msg['to_addr']] 48 | assert envelope._from == msg['from_addr'] 49 | assert envelope._subject == msg['subject'] 50 | assert len(envelope._parts) == 2 51 | 52 | text_part = envelope._parts[0] 53 | assert text_part[0] == 'text/plain' 54 | assert text_part[1] == msg['text_body'] 55 | assert text_part[2] == msg['charset'] 56 | 57 | html_part = envelope._parts[1] 58 | assert html_part[0] == 'text/html' 59 | assert html_part[1] == msg['html_body'] 60 | assert html_part[2] == msg['charset'] 61 | 62 | assert envelope._cc == msg['cc_addr'] 63 | assert envelope._bcc == msg['bcc_addr'] 64 | assert envelope._headers == msg['headers'] 65 | assert envelope._charset == msg['charset'] 66 | 67 | def test_addr_tuple_to_addr(self): 68 | addr = Envelope()._addr_tuple_to_addr(('test@example.com', 'Test')) 69 | assert addr == 'Test ' 70 | 71 | addr = Envelope(charset='utf-8')._addr_tuple_to_addr(( 72 | 'test@example.com', )) 73 | assert addr == 'test@example.com' 74 | 75 | def test_addrs_to_header(self): 76 | addrs = [ 77 | 'test1@example.com', 78 | 'Test2 ', 79 | ('test3@example.com', 'Test3'), 80 | ] 81 | 82 | header = Envelope()._addrs_to_header(addrs) 83 | ok_header = ( 84 | 'test1@example.com,' 85 | 'Test2 ,' 86 | 'Test3 ' 87 | ) 88 | 89 | assert header == ok_header 90 | 91 | try: 92 | header = Envelope()._addrs_to_header([1]) 93 | except MessageEncodeError as exc: 94 | assert exc.args[0] == '1 is not a valid address' 95 | except: 96 | raise 97 | else: 98 | assert False, "MessageEncodeError not raised" 99 | 100 | def test_raise(self): 101 | try: 102 | Envelope()._raise(RuntimeError, u'ęóąśłżźćń') 103 | except RuntimeError as exc: 104 | assert exc.args[0] == encoded(u'ęóąśłżźćń', 'utf-8') 105 | except: 106 | raise 107 | else: 108 | assert 'RuntimeError not raised' 109 | 110 | def test_to_mime_message_with_data(self): 111 | msg = self._dummy_message() 112 | envelope = Envelope(**msg) 113 | 114 | mime_msg = envelope.to_mime_message() 115 | assert mime_msg is not None 116 | 117 | assert mime_msg['Subject'] == msg['subject'] 118 | assert mime_msg['To'] == 'Example To ' 119 | assert mime_msg['From'] == 'Example From ' 120 | 121 | cc_header = ( 122 | 'cc1@example.com,' 123 | 'Example CC2 ,' 124 | 'Example CC3 ' 125 | ) 126 | assert mime_msg['CC'] == cc_header 127 | assert 'BCC' not in mime_msg 128 | 129 | assert mime_msg['Reply-To'] == msg['headers']['Reply-To'] 130 | assert mime_msg['X-Mailer'] == msg['headers']['X-Mailer'] 131 | 132 | mime_msg_parts = [part for part in mime_msg.walk()] 133 | assert len(mime_msg_parts) == 3 134 | text_part, html_part = mime_msg_parts[1:] 135 | 136 | assert text_part.get_content_type() == 'text/plain' 137 | assert text_part.get_payload(decode=True) == msg['text_body'].encode('utf-8') 138 | 139 | assert html_part.get_content_type() == 'text/html' 140 | assert html_part.get_payload(decode=True) == msg['html_body'].encode('utf-8') 141 | 142 | def test_to_mime_message_with_many_to_addresses(self): 143 | msg = self._dummy_message() 144 | msg['to_addr'] = [ 145 | 'to1@example.com', 146 | 'Example To2 ', 147 | ('to3@example.com', 'Example To3') 148 | ] 149 | envelope = Envelope(**msg) 150 | 151 | mime_msg = envelope.to_mime_message() 152 | assert mime_msg is not None 153 | 154 | to_header = ( 155 | 'to1@example.com,' 156 | 'Example To2 ,' 157 | 'Example To3 ' 158 | ) 159 | assert mime_msg['To'] == to_header 160 | 161 | def test_to_mime_message_with_no_data(self): 162 | envelope = Envelope() 163 | mime_msg = envelope.to_mime_message() 164 | 165 | assert mime_msg['Subject'] == '' 166 | assert mime_msg['To'] == '' 167 | assert mime_msg['From'] == '' 168 | 169 | assert 'CC' not in mime_msg 170 | assert 'BCC' not in mime_msg 171 | 172 | mime_msg_parts = [part for part in mime_msg.walk()] 173 | assert len(mime_msg_parts) == 1 174 | 175 | def test_to_mime_message_unicode(self): 176 | msg = { 177 | 'to_addr': ('to@example.com', u'ęóąśłżźćń'), 178 | 'from_addr': ('from@example.com', u'ęóąśłżźćń'), 179 | 'subject': u'ęóąśłżźćń', 180 | 'html_body': u'ęóąśłżźćń', 181 | 'text_body': u'ęóąśłżźćń', 182 | 'cc_addr': [ 183 | ('cc@example.com', u'ęóąśłżźćń') 184 | ], 185 | 'bcc_addr': [ 186 | u'ęóąśłżźćń ' 187 | ], 188 | 'headers': { 189 | 'X-Test': u'ęóąśłżźćń' 190 | }, 191 | 'charset': 'utf-8' 192 | } 193 | 194 | envelope = Envelope(**msg) 195 | 196 | def enc_addr_header(name, email): 197 | header = Header(name) 198 | header.append(email) 199 | return header.encode() 200 | 201 | mime_msg = envelope.to_mime_message() 202 | assert mime_msg is not None 203 | 204 | assert mime_msg['Subject'] == Header(msg['subject'], 'utf-8').encode() 205 | assert mime_msg['To'] == enc_addr_header(u'ęóąśłżźćń', '') 206 | assert mime_msg['From'] == enc_addr_header(u'ęóąśłżźćń', '') 207 | 208 | assert mime_msg['CC'] == enc_addr_header(u'ęóąśłżźćń', '') 209 | 210 | assert 'BCC' not in mime_msg 211 | 212 | assert mime_msg['X-Test'] == Header(msg['headers']['X-Test'], 'utf-8').encode() 213 | 214 | mime_msg_parts = [part for part in mime_msg.walk()] 215 | assert len(mime_msg_parts) == 3 216 | text_part, html_part = mime_msg_parts[1:] 217 | 218 | assert text_part.get_content_type() == 'text/plain' 219 | assert text_part.get_payload(decode=True) == msg['text_body'].encode('utf-8') 220 | 221 | assert html_part.get_content_type() == 'text/html' 222 | assert html_part.get_payload(decode=True) == msg['html_body'].encode('utf-8') 223 | 224 | def test_send(self): 225 | envelope = Envelope( 226 | from_addr='spam@example.com', 227 | to_addr='eggs@example.com', 228 | subject='Testing envelopes!', 229 | text_body='Just a testy test.' 230 | ) 231 | 232 | conn, result = envelope.send(host='localhost') 233 | assert conn._conn is not None 234 | assert len(conn._conn._call_stack.get('sendmail', [])) == 1 235 | 236 | def test_to_addr_property(self): 237 | msg = self._dummy_message() 238 | 239 | envelope = Envelope(**msg) 240 | assert envelope.to_addr == envelope._to 241 | 242 | msg.pop('to_addr') 243 | envelope = Envelope(**msg) 244 | assert envelope.to_addr == [] 245 | 246 | def test_add_to_addr(self): 247 | msg = self._dummy_message() 248 | msg.pop('to_addr') 249 | 250 | envelope = Envelope(**msg) 251 | envelope.add_to_addr('to2@example.com') 252 | assert envelope.to_addr == ['to2@example.com'] 253 | 254 | def test_clear_to_addr(self): 255 | msg = self._dummy_message() 256 | 257 | envelope = Envelope(**msg) 258 | envelope.clear_to_addr() 259 | assert envelope.to_addr == [] 260 | 261 | def test_from_addr_property(self): 262 | envelope = Envelope(**self._dummy_message()) 263 | assert envelope.from_addr == envelope._from 264 | 265 | envelope.from_addr = 'new@example.com' 266 | assert envelope.from_addr == 'new@example.com' 267 | 268 | def test_cc_addr_property(self): 269 | msg = self._dummy_message() 270 | 271 | envelope = Envelope(**msg) 272 | assert envelope.cc_addr == envelope._cc 273 | 274 | msg.pop('cc_addr') 275 | envelope = Envelope(**msg) 276 | assert envelope.cc_addr == [] 277 | 278 | def test_add_cc_addr(self): 279 | msg = self._dummy_message() 280 | msg.pop('cc_addr') 281 | 282 | envelope = Envelope(**msg) 283 | envelope.add_cc_addr('cc@example.com') 284 | assert envelope.cc_addr == ['cc@example.com'] 285 | 286 | def test_clear_cc_addr(self): 287 | msg = self._dummy_message() 288 | 289 | envelope = Envelope(**msg) 290 | envelope.clear_cc_addr() 291 | assert envelope.cc_addr == [] 292 | 293 | def test_bcc_addr_property(self): 294 | msg = self._dummy_message() 295 | 296 | envelope = Envelope(**msg) 297 | assert envelope.bcc_addr == envelope._bcc 298 | 299 | msg.pop('bcc_addr') 300 | envelope = Envelope(**msg) 301 | assert envelope.bcc_addr == [] 302 | 303 | def test_add_bcc_addr(self): 304 | msg = self._dummy_message() 305 | msg.pop('bcc_addr') 306 | 307 | envelope = Envelope(**msg) 308 | envelope.add_bcc_addr('bcc@example.com') 309 | assert envelope.bcc_addr == ['bcc@example.com'] 310 | 311 | def test_clear_bcc_addr(self): 312 | msg = self._dummy_message() 313 | 314 | envelope = Envelope(**msg) 315 | envelope.clear_bcc_addr() 316 | assert envelope.bcc_addr == [] 317 | 318 | def test_charset_property(self): 319 | envelope = Envelope() 320 | assert envelope.charset == envelope._charset 321 | 322 | envelope.charset = 'latin2' 323 | assert envelope._charset == 'latin2' 324 | 325 | def test_headers_property(self): 326 | msg = self._dummy_message() 327 | envelope = Envelope(**msg) 328 | 329 | assert envelope.headers == msg['headers'] 330 | 331 | def test_add_header(self): 332 | msg = self._dummy_message() 333 | msg.pop('headers') 334 | envelope = Envelope(**msg) 335 | 336 | envelope.add_header('X-Spam', 'eggs') 337 | assert envelope.headers == {'X-Spam': 'eggs'} 338 | 339 | def test_clear_headers(self): 340 | msg = self._dummy_message() 341 | envelope = Envelope(**msg) 342 | 343 | envelope.clear_headers() 344 | assert envelope.headers == {} 345 | 346 | def test_add_attachment(self): 347 | msg = self._dummy_message() 348 | envelope = Envelope(**msg) 349 | 350 | _jpg = self._tempfile(suffix='.jpg') 351 | envelope.add_attachment(_jpg) 352 | 353 | _mp3 = self._tempfile(suffix='.mp3') 354 | envelope.add_attachment(_mp3) 355 | 356 | _pdf = self._tempfile(suffix='.pdf') 357 | envelope.add_attachment(_pdf) 358 | 359 | _something = self._tempfile(suffix='.something', prefix=u'ęóąśłżźćń') 360 | envelope.add_attachment(_something) 361 | 362 | _octet = self._tempfile(suffix='.txt') 363 | envelope.add_attachment(_octet, mimetype='application/octet-stream') 364 | 365 | assert len(envelope._parts) == 7 366 | 367 | assert envelope._parts[0][0] == 'text/plain' 368 | assert envelope._parts[1][0] == 'text/html' 369 | 370 | assert envelope._parts[2][0] == 'image/jpeg' 371 | assert envelope._parts[2][1]['Content-Disposition'] ==\ 372 | 'attachment; filename="%s"' % os.path.basename(_jpg) 373 | 374 | assert envelope._parts[3][0] == 'audio/mpeg' 375 | assert envelope._parts[3][1]['Content-Disposition'] ==\ 376 | 'attachment; filename="%s"' % os.path.basename(_mp3) 377 | 378 | assert envelope._parts[4][0] == 'application/pdf' 379 | assert envelope._parts[4][1]['Content-Disposition'] ==\ 380 | 'attachment; filename="%s"' % os.path.basename(_pdf) 381 | 382 | assert envelope._parts[5][0] == 'application/octet-stream' 383 | assert envelope._parts[5][1]['Content-Disposition'] ==\ 384 | 'attachment; filename="%s"' %\ 385 | os.path.basename(encoded(_something, 'utf-8')) 386 | 387 | assert envelope._parts[6][0] == 'application/octet-stream' 388 | assert envelope._parts[6][1]['Content-Disposition'] ==\ 389 | 'attachment; filename="%s"' % os.path.basename(_octet) 390 | 391 | def test_repr(self): 392 | msg = self._dummy_message() 393 | envelope = Envelope(**msg) 394 | 395 | assert envelope.__repr__() == ( 396 | u"""""" 399 | ) 400 | --------------------------------------------------------------------------------