├── README.eclispe ├── README.md └── pyzmail ├── .gitignore ├── Changelog.txt ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.txt ├── build_exe.bat ├── distribute_setup.py ├── docs ├── Makefile └── source │ ├── _static │ ├── favicon16.ico │ ├── favicon24.ico │ ├── favicon32.ico │ ├── python-3.png │ ├── pyzmail-200.png │ ├── pyzmail-logo-small.png │ ├── sphinxdoc.css_t │ ├── tux-trans-32.png │ └── tux-transparent-56x64b.png │ ├── _templates │ └── layout.html │ ├── conf.py │ ├── index.rst │ └── man │ ├── pyzinfomail.rst │ └── pyzsendmail.rst ├── epydoc.css ├── pyzmail ├── __init__.py ├── generate.py ├── parse.py ├── tests │ ├── __init__.py │ ├── test_both.py │ ├── test_generate.py │ ├── test_parse.py │ ├── test_send.py │ └── test_utils.py ├── utils.py └── version.py ├── samples ├── b_minimal.eml ├── h_from_named.eml ├── h_from_q_iso.eml ├── h_multi_mixed_recipients.eml ├── h_subject_b_utf8.eml ├── h_subject_q_iso_8858_1.eml ├── m_eb_txt_html_png.eml ├── m_txt_ebhtml_png.eml ├── m_txt_html.eml ├── m_txt_html_png.eml ├── non_delivery1.eml ├── test.eml ├── test2.eml └── test_orig.eml ├── scripts ├── pyzinfomail └── pyzsendmail └── setup.py /README.eclispe: -------------------------------------------------------------------------------- 1 | Eclipse don't like the .git directory. 2 | The extra directory level is used to keep the to 3 | a level above the eclipse. 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyzmail 2 | ======= 3 | 4 | Pyzmail is a high level mail library for Python, providing functions to read, compose and send emails. 5 | Pyzmail hides the problems of using multiple encoding and the complexity of the MIME structure encoding/decoding. 6 | Pyzmail is available for python 2.6+ and 3.2+ under the LGPL license. 7 | 8 | More info here: http://www.magiksys.net/pyzmail 9 | 10 | 11 | -------------------------------------------------------------------------------- /pyzmail/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .settings/ 3 | .project 4 | .pydevproject 5 | __pycache__/ 6 | build/ 7 | docs/build 8 | dist/ 9 | man/ 10 | pyzinfomail.exe 11 | pyzsendmail.exe 12 | pyzmail.egg-info/ 13 | upload.lftp 14 | 15 | -------------------------------------------------------------------------------- /pyzmail/Changelog.txt: -------------------------------------------------------------------------------- 1 | 2015-xx-xx pyzmail-1.0.4 2 | all: 3 | - remove trailing white spaces in all sources and doc files 4 | generate.py: 5 | - header values can be email.header.Header instances (or unicode) 6 | pyzsendmail: 7 | - fix: binary attachments truncated on Windows 8 | - fix: It is now possible to specify a path including a ':' in -a and -e 9 | options by adding a trailing ':' 10 | 11 | 2014-05-30 pyzmail-1.0.3 12 | all: 13 | - upgarde distribute_setup.py up to 0.6.49, to see if it work better with python 3.4 14 | not tested 15 | 16 | 2013-07-07 pyzmail-1.0.2 17 | parse.py: 18 | - fix a bug: AttributeError: 'str' object has no attribute 'get_content_type' 19 | File "parse.py" line 333, in _search_message_content 20 | when parsing a badly formated 'multipart' email 21 | thanks to lyeph 22 | 23 | 2013-05-05 pyzmail-1.0.1 24 | parse.py: 25 | - fix: "parse.py filename" raise an error, PyzMessage 26 | don't accept a as argument. 27 | 28 | 2013-01-03 pyzmail-1.0.0 29 | generate: 30 | python3.x function smtp.login() require string 31 | 32 | documentation: 33 | - gmail server require 'tls' mode for sending 34 | 35 | 2011-09-16 pyzmail-0.9.9 36 | pyzsendmail: 37 | - small bug about arg_charset 38 | 39 | 40 | 2011-09-13 pyzmail-0.9.8 41 | all: 42 | - support for python 3.2 43 | - added tests module 44 | - some documentation cosmetic 45 | 46 | pyzsendmail: 47 | - removed a old debug message (print) 48 | - return 0 for success, 1 for error, 2 for failed addresses 49 | 50 | generate: 51 | - Message-ID instead of Messsage-ID with 3 's' 52 | - add support for free header fields 53 | 54 | parse: 55 | - PzMessage renamed into PyzMessage, PzMessage is now deprecated 56 | 57 | 2011-08-21 pyzmail-0.9.0 58 | - first release 59 | 60 | -------------------------------------------------------------------------------- /pyzmail/LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /pyzmail/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.txt Changelog.txt LICENSE.txt 2 | include Makefile MANIFEST.in epydoc.css 3 | include distribute_setup.py 4 | include man/* 5 | include docs/Makefile 6 | include docs/source/* 7 | -------------------------------------------------------------------------------- /pyzmail/Makefile: -------------------------------------------------------------------------------- 1 | epydoc_target=docs/build/html/api 2 | python3=/usr/bin/python3.4 3 | 2to3=/usr/bin/2to3-3.4 4 | 5 | epydoc: 6 | mkdir -p ${epydoc_target} 7 | #--google-analytics "UA-21029552-4" 8 | epydoc -v -c epydoc.css --google-analytics "UA-21029552-4" -u ../index.html -n "pyzmail homepage" --html pyzmail -o ${epydoc_target} 9 | 10 | sphinx: 11 | cd docs ; make html man 12 | cp ./docs/build/man/* man 13 | cp ./docs/build/html/man/* man 14 | 15 | test: 16 | python setup.py test 17 | 18 | test3: 19 | sudo rm -rf build ; ${python3} setup.py test 20 | 21 | py3: 22 | mkdir -p py3k 23 | rm -rf py3k/* 24 | cp scripts/* py3k 25 | ${2to3} --no-diffs --write --nobackups py3k/* 26 | find py3k -type f -exec perl -pi -e "s=#!/bin/env python=#!${python3}=g" {} \; 27 | chmod a+x py3k/* 28 | cp -av pyzmail py3k 29 | ${2to3} --no-diffs --write --nobackups py3k 30 | 31 | all: epydoc sphinx upload 32 | 33 | docs: epydoc sphinx 34 | 35 | clean: 36 | rm -rf build 37 | rm -rf docs/build 38 | rm -rf py3k 39 | rm -rf pyzmail/__pycache__ 40 | rm -rf pyzmail/tests/__pycache__ 41 | find . -name "*.pyc" -exec rm {} \; 42 | 43 | upload: 44 | cp ./docs/build/html/_static/favicon32.ico docs/build/html/favicon.ico 45 | lftp -f upload.lftp 46 | 47 | sdist: sphinx 48 | python setup.py sdist 49 | 50 | pypi: sphinx 51 | python setup.py sdist upload 52 | 53 | -------------------------------------------------------------------------------- /pyzmail/README.txt: -------------------------------------------------------------------------------- 1 | pyzmail library 2 | ============== 3 | 4 | pyzmail: Python eazy mail library 5 | Copyright: Alain Spineux 6 | License: LGPL 7 | Homepage: http://www.magiksys.net/pyzmail/ 8 | 9 | Overview 10 | -------- 11 | 12 | pyzmail is a high level mail library for Python. It provides functions and 13 | classes that help to read, compose and send emails. pyzmail exists because 14 | their is no reasons that handling mails with Python would be more difficult 15 | than with popular mail clients like Outlook or Thunderbird. pyzmail hide the 16 | difficulties of the MIME structure and MIME encoding/decoding. It also hide 17 | the problem of the internationalized header encoding/decoding. 18 | 19 | 20 | -------------------------------------------------------------------------------- /pyzmail/build_exe.bat: -------------------------------------------------------------------------------- 1 | c:\Python26\python.exe setup.py py2exe --single-file 2 | -------------------------------------------------------------------------------- /pyzmail/distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import shutil 18 | import sys 19 | import time 20 | import fnmatch 21 | import tempfile 22 | import tarfile 23 | import optparse 24 | 25 | from distutils import log 26 | 27 | try: 28 | from site import USER_SITE 29 | except ImportError: 30 | USER_SITE = None 31 | 32 | try: 33 | import subprocess 34 | 35 | def _python_cmd(*args): 36 | args = (sys.executable,) + args 37 | return subprocess.call(args) == 0 38 | 39 | except ImportError: 40 | # will be used for python 2.3 41 | def _python_cmd(*args): 42 | args = (sys.executable,) + args 43 | # quoting arguments if windows 44 | if sys.platform == 'win32': 45 | def quote(arg): 46 | if ' ' in arg: 47 | return '"%s"' % arg 48 | return arg 49 | args = [quote(arg) for arg in args] 50 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 51 | 52 | DEFAULT_VERSION = "0.6.49" 53 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 54 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 55 | 56 | SETUPTOOLS_PKG_INFO = """\ 57 | Metadata-Version: 1.0 58 | Name: setuptools 59 | Version: %s 60 | Summary: xxxx 61 | Home-page: xxx 62 | Author: xxx 63 | Author-email: xxx 64 | License: xxx 65 | Description: xxx 66 | """ % SETUPTOOLS_FAKED_VERSION 67 | 68 | 69 | def _install(tarball, install_args=()): 70 | # extracting the tarball 71 | tmpdir = tempfile.mkdtemp() 72 | log.warn('Extracting in %s', tmpdir) 73 | old_wd = os.getcwd() 74 | try: 75 | os.chdir(tmpdir) 76 | tar = tarfile.open(tarball) 77 | _extractall(tar) 78 | tar.close() 79 | 80 | # going in the directory 81 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 82 | os.chdir(subdir) 83 | log.warn('Now working in %s', subdir) 84 | 85 | # installing 86 | log.warn('Installing Distribute') 87 | if not _python_cmd('setup.py', 'install', *install_args): 88 | log.warn('Something went wrong during the installation.') 89 | log.warn('See the error message above.') 90 | # exitcode will be 2 91 | return 2 92 | finally: 93 | os.chdir(old_wd) 94 | shutil.rmtree(tmpdir) 95 | 96 | 97 | def _build_egg(egg, tarball, to_dir): 98 | # extracting the tarball 99 | tmpdir = tempfile.mkdtemp() 100 | log.warn('Extracting in %s', tmpdir) 101 | old_wd = os.getcwd() 102 | try: 103 | os.chdir(tmpdir) 104 | tar = tarfile.open(tarball) 105 | _extractall(tar) 106 | tar.close() 107 | 108 | # going in the directory 109 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 110 | os.chdir(subdir) 111 | log.warn('Now working in %s', subdir) 112 | 113 | # building an egg 114 | log.warn('Building a Distribute egg in %s', to_dir) 115 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 116 | 117 | finally: 118 | os.chdir(old_wd) 119 | shutil.rmtree(tmpdir) 120 | # returning the result 121 | log.warn(egg) 122 | if not os.path.exists(egg): 123 | raise IOError('Could not build the egg.') 124 | 125 | 126 | def _do_download(version, download_base, to_dir, download_delay): 127 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 128 | % (version, sys.version_info[0], sys.version_info[1])) 129 | if not os.path.exists(egg): 130 | tarball = download_setuptools(version, download_base, 131 | to_dir, download_delay) 132 | _build_egg(egg, tarball, to_dir) 133 | sys.path.insert(0, egg) 134 | import setuptools 135 | setuptools.bootstrap_install_from = egg 136 | 137 | 138 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 139 | to_dir=os.curdir, download_delay=15, no_fake=True): 140 | # making sure we use the absolute path 141 | to_dir = os.path.abspath(to_dir) 142 | was_imported = 'pkg_resources' in sys.modules or \ 143 | 'setuptools' in sys.modules 144 | try: 145 | try: 146 | import pkg_resources 147 | 148 | # Setuptools 0.7b and later is a suitable (and preferable) 149 | # substitute for any Distribute version. 150 | try: 151 | pkg_resources.require("setuptools>=0.7b") 152 | return 153 | except (pkg_resources.DistributionNotFound, 154 | pkg_resources.VersionConflict): 155 | pass 156 | 157 | if not hasattr(pkg_resources, '_distribute'): 158 | if not no_fake: 159 | _fake_setuptools() 160 | raise ImportError 161 | except ImportError: 162 | return _do_download(version, download_base, to_dir, download_delay) 163 | try: 164 | pkg_resources.require("distribute>=" + version) 165 | return 166 | except pkg_resources.VersionConflict: 167 | e = sys.exc_info()[1] 168 | if was_imported: 169 | sys.stderr.write( 170 | "The required version of distribute (>=%s) is not available,\n" 171 | "and can't be installed while this script is running. Please\n" 172 | "install a more recent version first, using\n" 173 | "'easy_install -U distribute'." 174 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 175 | sys.exit(2) 176 | else: 177 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 178 | return _do_download(version, download_base, to_dir, 179 | download_delay) 180 | except pkg_resources.DistributionNotFound: 181 | return _do_download(version, download_base, to_dir, 182 | download_delay) 183 | finally: 184 | if not no_fake: 185 | _create_fake_setuptools_pkg_info(to_dir) 186 | 187 | 188 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 189 | to_dir=os.curdir, delay=15): 190 | """Download distribute from a specified location and return its filename 191 | 192 | `version` should be a valid distribute version number that is available 193 | as an egg for download under the `download_base` URL (which should end 194 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 195 | `delay` is the number of seconds to pause before an actual download 196 | attempt. 197 | """ 198 | # making sure we use the absolute path 199 | to_dir = os.path.abspath(to_dir) 200 | try: 201 | from urllib.request import urlopen 202 | except ImportError: 203 | from urllib2 import urlopen 204 | tgz_name = "distribute-%s.tar.gz" % version 205 | url = download_base + tgz_name 206 | saveto = os.path.join(to_dir, tgz_name) 207 | src = dst = None 208 | if not os.path.exists(saveto): # Avoid repeated downloads 209 | try: 210 | log.warn("Downloading %s", url) 211 | src = urlopen(url) 212 | # Read/write all in one block, so we don't create a corrupt file 213 | # if the download is interrupted. 214 | data = src.read() 215 | dst = open(saveto, "wb") 216 | dst.write(data) 217 | finally: 218 | if src: 219 | src.close() 220 | if dst: 221 | dst.close() 222 | return os.path.realpath(saveto) 223 | 224 | 225 | def _no_sandbox(function): 226 | def __no_sandbox(*args, **kw): 227 | try: 228 | from setuptools.sandbox import DirectorySandbox 229 | if not hasattr(DirectorySandbox, '_old'): 230 | def violation(*args): 231 | pass 232 | DirectorySandbox._old = DirectorySandbox._violation 233 | DirectorySandbox._violation = violation 234 | patched = True 235 | else: 236 | patched = False 237 | except ImportError: 238 | patched = False 239 | 240 | try: 241 | return function(*args, **kw) 242 | finally: 243 | if patched: 244 | DirectorySandbox._violation = DirectorySandbox._old 245 | del DirectorySandbox._old 246 | 247 | return __no_sandbox 248 | 249 | 250 | def _patch_file(path, content): 251 | """Will backup the file then patch it""" 252 | f = open(path) 253 | existing_content = f.read() 254 | f.close() 255 | if existing_content == content: 256 | # already patched 257 | log.warn('Already patched.') 258 | return False 259 | log.warn('Patching...') 260 | _rename_path(path) 261 | f = open(path, 'w') 262 | try: 263 | f.write(content) 264 | finally: 265 | f.close() 266 | return True 267 | 268 | _patch_file = _no_sandbox(_patch_file) 269 | 270 | 271 | def _same_content(path, content): 272 | f = open(path) 273 | existing_content = f.read() 274 | f.close() 275 | return existing_content == content 276 | 277 | 278 | def _rename_path(path): 279 | new_name = path + '.OLD.%s' % time.time() 280 | log.warn('Renaming %s to %s', path, new_name) 281 | os.rename(path, new_name) 282 | return new_name 283 | 284 | 285 | def _remove_flat_installation(placeholder): 286 | if not os.path.isdir(placeholder): 287 | log.warn('Unkown installation at %s', placeholder) 288 | return False 289 | found = False 290 | for file in os.listdir(placeholder): 291 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 292 | found = True 293 | break 294 | if not found: 295 | log.warn('Could not locate setuptools*.egg-info') 296 | return 297 | 298 | log.warn('Moving elements out of the way...') 299 | pkg_info = os.path.join(placeholder, file) 300 | if os.path.isdir(pkg_info): 301 | patched = _patch_egg_dir(pkg_info) 302 | else: 303 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 304 | 305 | if not patched: 306 | log.warn('%s already patched.', pkg_info) 307 | return False 308 | # now let's move the files out of the way 309 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 310 | element = os.path.join(placeholder, element) 311 | if os.path.exists(element): 312 | _rename_path(element) 313 | else: 314 | log.warn('Could not find the %s element of the ' 315 | 'Setuptools distribution', element) 316 | return True 317 | 318 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 319 | 320 | 321 | def _after_install(dist): 322 | log.warn('After install bootstrap.') 323 | placeholder = dist.get_command_obj('install').install_purelib 324 | _create_fake_setuptools_pkg_info(placeholder) 325 | 326 | 327 | def _create_fake_setuptools_pkg_info(placeholder): 328 | if not placeholder or not os.path.exists(placeholder): 329 | log.warn('Could not find the install location') 330 | return 331 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 332 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 333 | (SETUPTOOLS_FAKED_VERSION, pyver) 334 | pkg_info = os.path.join(placeholder, setuptools_file) 335 | if os.path.exists(pkg_info): 336 | log.warn('%s already exists', pkg_info) 337 | return 338 | 339 | log.warn('Creating %s', pkg_info) 340 | try: 341 | f = open(pkg_info, 'w') 342 | except EnvironmentError: 343 | log.warn("Don't have permissions to write %s, skipping", pkg_info) 344 | return 345 | try: 346 | f.write(SETUPTOOLS_PKG_INFO) 347 | finally: 348 | f.close() 349 | 350 | pth_file = os.path.join(placeholder, 'setuptools.pth') 351 | log.warn('Creating %s', pth_file) 352 | f = open(pth_file, 'w') 353 | try: 354 | f.write(os.path.join(os.curdir, setuptools_file)) 355 | finally: 356 | f.close() 357 | 358 | _create_fake_setuptools_pkg_info = _no_sandbox( 359 | _create_fake_setuptools_pkg_info 360 | ) 361 | 362 | 363 | def _patch_egg_dir(path): 364 | # let's check if it's already patched 365 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 366 | if os.path.exists(pkg_info): 367 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 368 | log.warn('%s already patched.', pkg_info) 369 | return False 370 | _rename_path(path) 371 | os.mkdir(path) 372 | os.mkdir(os.path.join(path, 'EGG-INFO')) 373 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 374 | f = open(pkg_info, 'w') 375 | try: 376 | f.write(SETUPTOOLS_PKG_INFO) 377 | finally: 378 | f.close() 379 | return True 380 | 381 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 382 | 383 | 384 | def _before_install(): 385 | log.warn('Before install bootstrap.') 386 | _fake_setuptools() 387 | 388 | 389 | def _under_prefix(location): 390 | if 'install' not in sys.argv: 391 | return True 392 | args = sys.argv[sys.argv.index('install') + 1:] 393 | for index, arg in enumerate(args): 394 | for option in ('--root', '--prefix'): 395 | if arg.startswith('%s=' % option): 396 | top_dir = arg.split('root=')[-1] 397 | return location.startswith(top_dir) 398 | elif arg == option: 399 | if len(args) > index: 400 | top_dir = args[index + 1] 401 | return location.startswith(top_dir) 402 | if arg == '--user' and USER_SITE is not None: 403 | return location.startswith(USER_SITE) 404 | return True 405 | 406 | 407 | def _fake_setuptools(): 408 | log.warn('Scanning installed packages') 409 | try: 410 | import pkg_resources 411 | except ImportError: 412 | # we're cool 413 | log.warn('Setuptools or Distribute does not seem to be installed.') 414 | return 415 | ws = pkg_resources.working_set 416 | try: 417 | setuptools_dist = ws.find( 418 | pkg_resources.Requirement.parse('setuptools', replacement=False) 419 | ) 420 | except TypeError: 421 | # old distribute API 422 | setuptools_dist = ws.find( 423 | pkg_resources.Requirement.parse('setuptools') 424 | ) 425 | 426 | if setuptools_dist is None: 427 | log.warn('No setuptools distribution found') 428 | return 429 | # detecting if it was already faked 430 | setuptools_location = setuptools_dist.location 431 | log.warn('Setuptools installation detected at %s', setuptools_location) 432 | 433 | # if --root or --preix was provided, and if 434 | # setuptools is not located in them, we don't patch it 435 | if not _under_prefix(setuptools_location): 436 | log.warn('Not patching, --root or --prefix is installing Distribute' 437 | ' in another location') 438 | return 439 | 440 | # let's see if its an egg 441 | if not setuptools_location.endswith('.egg'): 442 | log.warn('Non-egg installation') 443 | res = _remove_flat_installation(setuptools_location) 444 | if not res: 445 | return 446 | else: 447 | log.warn('Egg installation') 448 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 449 | if (os.path.exists(pkg_info) and 450 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 451 | log.warn('Already patched.') 452 | return 453 | log.warn('Patching...') 454 | # let's create a fake egg replacing setuptools one 455 | res = _patch_egg_dir(setuptools_location) 456 | if not res: 457 | return 458 | log.warn('Patching complete.') 459 | _relaunch() 460 | 461 | 462 | def _relaunch(): 463 | log.warn('Relaunching...') 464 | # we have to relaunch the process 465 | # pip marker to avoid a relaunch bug 466 | _cmd1 = ['-c', 'install', '--single-version-externally-managed'] 467 | _cmd2 = ['-c', 'install', '--record'] 468 | if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2: 469 | sys.argv[0] = 'setup.py' 470 | args = [sys.executable] + sys.argv 471 | sys.exit(subprocess.call(args)) 472 | 473 | 474 | def _extractall(self, path=".", members=None): 475 | """Extract all members from the archive to the current working 476 | directory and set owner, modification time and permissions on 477 | directories afterwards. `path' specifies a different directory 478 | to extract to. `members' is optional and must be a subset of the 479 | list returned by getmembers(). 480 | """ 481 | import copy 482 | import operator 483 | from tarfile import ExtractError 484 | directories = [] 485 | 486 | if members is None: 487 | members = self 488 | 489 | for tarinfo in members: 490 | if tarinfo.isdir(): 491 | # Extract directories with a safe mode. 492 | directories.append(tarinfo) 493 | tarinfo = copy.copy(tarinfo) 494 | tarinfo.mode = 448 # decimal for oct 0700 495 | self.extract(tarinfo, path) 496 | 497 | # Reverse sort directories. 498 | if sys.version_info < (2, 4): 499 | def sorter(dir1, dir2): 500 | return cmp(dir1.name, dir2.name) 501 | directories.sort(sorter) 502 | directories.reverse() 503 | else: 504 | directories.sort(key=operator.attrgetter('name'), reverse=True) 505 | 506 | # Set correct owner, mtime and filemode on directories. 507 | for tarinfo in directories: 508 | dirpath = os.path.join(path, tarinfo.name) 509 | try: 510 | self.chown(tarinfo, dirpath) 511 | self.utime(tarinfo, dirpath) 512 | self.chmod(tarinfo, dirpath) 513 | except ExtractError: 514 | e = sys.exc_info()[1] 515 | if self.errorlevel > 1: 516 | raise 517 | else: 518 | self._dbg(1, "tarfile: %s" % e) 519 | 520 | 521 | def _build_install_args(options): 522 | """ 523 | Build the arguments to 'python setup.py install' on the distribute package 524 | """ 525 | install_args = [] 526 | if options.user_install: 527 | if sys.version_info < (2, 6): 528 | log.warn("--user requires Python 2.6 or later") 529 | raise SystemExit(1) 530 | install_args.append('--user') 531 | return install_args 532 | 533 | def _parse_args(): 534 | """ 535 | Parse the command line for options 536 | """ 537 | parser = optparse.OptionParser() 538 | parser.add_option( 539 | '--user', dest='user_install', action='store_true', default=False, 540 | help='install in user site package (requires Python 2.6 or later)') 541 | parser.add_option( 542 | '--download-base', dest='download_base', metavar="URL", 543 | default=DEFAULT_URL, 544 | help='alternative URL from where to download the distribute package') 545 | options, args = parser.parse_args() 546 | # positional arguments are ignored 547 | return options 548 | 549 | def main(version=DEFAULT_VERSION): 550 | """Install or upgrade setuptools and EasyInstall""" 551 | options = _parse_args() 552 | tarball = download_setuptools(download_base=options.download_base) 553 | return _install(tarball, _build_install_args(options)) 554 | 555 | if __name__ == '__main__': 556 | sys.exit(main()) 557 | -------------------------------------------------------------------------------- /pyzmail/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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pythoneasymaillibrary.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pythoneasymaillibrary.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pythoneasymaillibrary" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pythoneasymaillibrary" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/favicon16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/favicon16.ico -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/favicon24.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/favicon24.ico -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/favicon32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/favicon32.ico -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/python-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/python-3.png -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/pyzmail-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/pyzmail-200.png -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/pyzmail-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/pyzmail-logo-small.png -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/sphinxdoc.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * sphinxdoc.css_t 3 | * ~~~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- sphinxdoc theme. Originally created by 6 | * Armin Ronacher for Werkzeug. 7 | * 8 | * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. 9 | * :license: BSD, see LICENSE for details. 10 | * 11 | */ 12 | 13 | @import url("basic.css"); 14 | 15 | /* -- page layout ----------------------------------------------------------- */ 16 | 17 | body { 18 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 19 | 'Verdana', sans-serif; 20 | font-size: 14px; 21 | letter-spacing: -0.01em; 22 | line-height: 150%; 23 | text-align: center; 24 | background-color: #BFD1D4; 25 | color: black; 26 | padding: 0; 27 | border: 1px solid #aaa; 28 | 29 | margin: 0px 80px 0px 80px; 30 | min-width: 740px; 31 | } 32 | 33 | div.document { 34 | background-color: white; 35 | text-align: left; 36 | background-image: url(contents.png); 37 | background-repeat: repeat-x; 38 | } 39 | 40 | div.bodywrapper { 41 | margin: 0 240px 0 0; 42 | border-right: 1px solid #ccc; 43 | } 44 | 45 | div.body { 46 | margin: 0; 47 | padding: 0.5em 20px 20px 20px; 48 | } 49 | 50 | /* ASX */ 51 | span.homesite { 52 | position: relative; 53 | top: -25px; 54 | left: 10px; 55 | } 56 | 57 | div.related { 58 | font-size: 1em; 59 | } 60 | 61 | div.related ul { 62 | background-image: url(navigation.png); 63 | height: 2em; 64 | border-top: 1px solid #ddd; 65 | border-bottom: 1px solid #ddd; 66 | } 67 | 68 | div.related ul li { 69 | margin: 0; 70 | padding: 0; 71 | height: 2em; 72 | float: left; 73 | } 74 | 75 | div.related ul li.right { 76 | float: right; 77 | margin-right: 5px; 78 | } 79 | 80 | div.related ul li a { 81 | margin: 0; 82 | padding: 0 5px 0 5px; 83 | line-height: 1.75em; 84 | color: #EE9816; 85 | } 86 | 87 | div.related ul li a:hover { 88 | color: #3CA8E7; 89 | } 90 | 91 | div.sphinxsidebarwrapper { 92 | padding: 0; 93 | } 94 | 95 | div.sphinxsidebar { 96 | margin: 0; 97 | padding: 0.5em 15px 15px 0; 98 | width: 210px; 99 | float: right; 100 | font-size: 1em; 101 | text-align: left; 102 | } 103 | 104 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 105 | margin: 1em 0 0.5em 0; 106 | font-size: 1em; 107 | padding: 0.1em 0 0.1em 0.5em; 108 | color: white; 109 | border: 1px solid #86989B; 110 | background-color: #AFC1C4; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: white; 115 | } 116 | 117 | div.sphinxsidebar ul { 118 | padding-left: 1.5em; 119 | margin-top: 7px; 120 | padding: 0; 121 | line-height: 130%; 122 | } 123 | 124 | div.sphinxsidebar ul ul { 125 | margin-left: 20px; 126 | } 127 | 128 | div.footer { 129 | background-color: #E3EFF1; 130 | color: #86989B; 131 | padding: 3px 8px 3px 0; 132 | clear: both; 133 | font-size: 0.8em; 134 | text-align: right; 135 | } 136 | 137 | div.footer a { 138 | color: #86989B; 139 | text-decoration: underline; 140 | } 141 | 142 | /* -- body styles ----------------------------------------------------------- */ 143 | 144 | p { 145 | margin: 0.8em 0 0.5em 0; 146 | } 147 | 148 | a { 149 | color: #CA7900; 150 | text-decoration: none; 151 | } 152 | 153 | a:hover { 154 | color: #2491CF; 155 | } 156 | 157 | div.body a { 158 | text-decoration: underline; 159 | } 160 | 161 | h1 { 162 | margin: 0; 163 | padding: 0.7em 0 0.3em 0; 164 | font-size: 1.5em; 165 | color: #11557C; 166 | } 167 | 168 | h2 { 169 | margin: 1.3em 0 0.2em 0; 170 | font-size: 1.35em; 171 | padding: 0; 172 | } 173 | 174 | h3 { 175 | margin: 1em 0 -0.3em 0; 176 | font-size: 1.2em; 177 | color: #606060; 178 | } 179 | 180 | div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { 181 | color: black!important; 182 | } 183 | 184 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 185 | display: none; 186 | margin: 0 0 0 0.3em; 187 | padding: 0 0.2em 0 0.2em; 188 | color: #aaa!important; 189 | } 190 | 191 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 192 | h5:hover a.anchor, h6:hover a.anchor { 193 | display: inline; 194 | } 195 | 196 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 197 | h5 a.anchor:hover, h6 a.anchor:hover { 198 | color: #777; 199 | background-color: #eee; 200 | } 201 | 202 | a.headerlink { 203 | color: #c60f0f!important; 204 | font-size: 1em; 205 | margin-left: 6px; 206 | padding: 0 4px 0 4px; 207 | text-decoration: none!important; 208 | } 209 | 210 | a.headerlink:hover { 211 | background-color: #ccc; 212 | color: white!important; 213 | } 214 | 215 | cite, code, tt { 216 | font-family: 'Consolas', 'Deja Vu Sans Mono', 217 | 'Bitstream Vera Sans Mono', monospace; 218 | font-size: 0.95em; 219 | letter-spacing: 0.01em; 220 | } 221 | 222 | tt { 223 | background-color: #f2f2f2; 224 | border-bottom: 1px solid #ddd; 225 | color: #333; 226 | } 227 | 228 | tt.descname, tt.descclassname, tt.xref { 229 | border: 0; 230 | } 231 | 232 | hr { 233 | border: 1px solid #abc; 234 | margin: 2em; 235 | } 236 | 237 | a tt { 238 | border: 0; 239 | color: #CA7900; 240 | } 241 | 242 | a tt:hover { 243 | color: #2491CF; 244 | } 245 | 246 | pre { 247 | font-family: 'Consolas', 'Deja Vu Sans Mono', 248 | 'Bitstream Vera Sans Mono', monospace; 249 | font-size: 0.95em; 250 | letter-spacing: 0.015em; 251 | line-height: 120%; 252 | padding: 0.5em; 253 | border: 1px solid #ccc; 254 | background-color: #f8f8f8; 255 | } 256 | 257 | pre a { 258 | color: inherit; 259 | text-decoration: underline; 260 | } 261 | 262 | td.linenos pre { 263 | padding: 0.5em 0; 264 | } 265 | 266 | div.quotebar { 267 | background-color: #f8f8f8; 268 | max-width: 250px; 269 | float: right; 270 | padding: 2px 7px; 271 | border: 1px solid #ccc; 272 | } 273 | 274 | div.topic { 275 | background-color: #f8f8f8; 276 | } 277 | 278 | table { 279 | border-collapse: collapse; 280 | margin: 0 -0.5em 0 -0.5em; 281 | } 282 | 283 | table td, table th { 284 | padding: 0.2em 0.5em 0.2em 0.5em; 285 | } 286 | 287 | div.admonition, div.warning { 288 | font-size: 0.9em; 289 | margin: 1em 0 1em 0; 290 | border: 1px solid #86989B; 291 | background-color: #f7f7f7; 292 | padding: 0; 293 | } 294 | 295 | div.admonition p, div.warning p { 296 | margin: 0.5em 1em 0.5em 1em; 297 | padding: 0; 298 | } 299 | 300 | div.admonition pre, div.warning pre { 301 | margin: 0.4em 1em 0.4em 1em; 302 | } 303 | 304 | div.admonition p.admonition-title, 305 | div.warning p.admonition-title { 306 | margin: 0; 307 | padding: 0.1em 0 0.1em 0.5em; 308 | color: white; 309 | border-bottom: 1px solid #86989B; 310 | font-weight: bold; 311 | background-color: #AFC1C4; 312 | } 313 | 314 | div.warning { 315 | border: 1px solid #940000; 316 | } 317 | 318 | div.warning p.admonition-title { 319 | background-color: #CF0000; 320 | border-bottom-color: #940000; 321 | } 322 | 323 | div.admonition ul, div.admonition ol, 324 | div.warning ul, div.warning ol { 325 | margin: 0.1em 0.5em 0.5em 3em; 326 | padding: 0; 327 | } 328 | 329 | div.versioninfo { 330 | margin: 1em 0 0 0; 331 | border: 1px solid #ccc; 332 | background-color: #DDEAF0; 333 | padding: 8px; 334 | line-height: 1.3em; 335 | font-size: 0.9em; 336 | } 337 | 338 | .viewcode-back { 339 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 340 | 'Verdana', sans-serif; 341 | } 342 | 343 | div.viewcode-block:target { 344 | background-color: #f4debf; 345 | border-top: 1px solid #ac9; 346 | border-bottom: 1px solid #ac9; 347 | } 348 | -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/tux-trans-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/tux-trans-32.png -------------------------------------------------------------------------------- /pyzmail/docs/source/_static/tux-transparent-56x64b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/docs/source/_static/tux-transparent-56x64b.png -------------------------------------------------------------------------------- /pyzmail/docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 18 | {% endblock %} 19 | 20 | {% block sidebarlogo %} 21 | {{ super() }} 22 |

Author's home

23 | 29 | {%- endblock %} 30 | 31 | -------------------------------------------------------------------------------- /pyzmail/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python easy mail library documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Aug 19 12:16:52 2011. 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', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] 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'Python easy mail library' 44 | copyright = u'2011, Alain Spineux' 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 | 52 | for line in open('../../pyzmail/version.py'): 53 | if line.startswith("__version__="): 54 | version=line[13:].rstrip()[:-1] 55 | break 56 | 57 | #version = '0.9' 58 | # The full version, including alpha/beta/rc tags. 59 | #release = '0.9.1' 60 | release=version 61 | version=version[:version.rfind('.')] 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = [] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all documents. 78 | #default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | #add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | #add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | show_authors = True 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | #modindex_common_prefix = [] 96 | 97 | 98 | # -- Options for HTML output --------------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | html_theme = 'sphinxdoc' 103 | 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | #html_theme_options = {} 108 | 109 | # Add any paths that contain custom themes here, relative to this directory. 110 | #html_theme_path = [] 111 | 112 | # The name for this set of Sphinx documents. If None, it defaults to 113 | # " v documentation". 114 | #html_title = 'pyzmail: Python easy mail library' 115 | 116 | # A shorter title for the navigation bar. Default is the same as html_title. 117 | html_short_title = 'pyzmail home' 118 | 119 | # The name of an image file (relative to this directory) to place at the top 120 | # of the sidebar. 121 | html_logo = 'source/_static/pyzmail-logo-small.png' 122 | html_tuxlogo = 'source/_static/tux-transparent-56x64b.png' 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | html_favicon = 'favicon32.ico' 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 135 | # using the given strftime format. 136 | #html_last_updated_fmt = '%b %d, %Y' 137 | 138 | # If true, SmartyPants will be used to convert quotes and dashes to 139 | # typographically correct entities. 140 | #html_use_smartypants = True 141 | 142 | # Custom sidebar templates, maps document names to template names. 143 | #html_sidebars = {} 144 | 145 | # Additional templates that should be rendered to pages, maps page names to 146 | # template names. 147 | #html_additional_pages = {} 148 | 149 | # If false, no module index is generated. 150 | #html_domain_indices = True 151 | 152 | # If false, no index is generated. 153 | html_use_index = True 154 | 155 | # If true, the index is split into individual pages for each letter. 156 | #html_split_index = False 157 | 158 | # If true, links to the reST sources are added to the pages. 159 | html_show_sourcelink = True 160 | 161 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 162 | #html_show_sphinx = True 163 | 164 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 165 | #html_show_copyright = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | #html_use_opensearch = '' 171 | 172 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 173 | #html_file_suffix = None 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = 'pyzmaildoc' 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | # The paper size ('letter' or 'a4'). 182 | #latex_paper_size = 'letter' 183 | 184 | # The font size ('10pt', '11pt' or '12pt'). 185 | #latex_font_size = '10pt' 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'pyzmail.tex', u'Python easy mail library Documentation', 191 | u'Alain Spineux', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Additional stuff for the LaTeX preamble. 209 | #latex_preamble = '' 210 | 211 | # Documents to append as an appendix to all manuals. 212 | #latex_appendices = [] 213 | 214 | # If false, no module index is generated. 215 | #latex_domain_indices = True 216 | 217 | 218 | # -- Options for manual page output -------------------------------------------- 219 | 220 | # One entry per manual page. List of tuples 221 | # (source start file, name, description, authors, manual section). 222 | man_pages = [ 223 | ('index', 'pyzmail', u'Python easy mail library Documentation', 224 | [u'Alain Spineux'], 1), 225 | ('man/pyzsendmail', 'pyzsendmail', 'command line tools to compose and send mail', 226 | '', 1), 227 | ('man/pyzinfomail', 'pyzinfomail', 'command line tools to retrieve info from mail', 228 | '', 1), 229 | 230 | ] 231 | -------------------------------------------------------------------------------- /pyzmail/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Python easy mail library documentation master file, created by 2 | sphinx-quickstart on Fri Aug 19 12:16:52 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. title:: pyzmail 7 | 8 | pyzmail: Python easy mail library 9 | ================================= 10 | 11 | **pyzmail** is a **high level** mail library for Python. It provides functions and 12 | classes that help for **reading**, **composing** and **sending** emails. **pyzmail** 13 | exists because their is no reasons that handling mails with Python would be more 14 | difficult than with popular mail clients like Outlook or Thunderbird. 15 | **pyzmail** hides the complexity of the MIME structure and MIME 16 | encoding/decoding. It also make the problems of the internationalization 17 | encoding/decoding simpler. 18 | 19 | Download and Install 20 | -------------------- 21 | 22 | **pyzmail** is available for Python **2.6+** and **3.2+** 23 | from `pypi `_ and can 24 | be easily installed using the `easy_install `_ 25 | successor named `distribute `_ 26 | and `pip `_ using :: 27 | 28 | $ pip install pyzmail 29 | 30 | to quickly install **distribute** and **pip**, use :: 31 | 32 | curl -O http://python-distribute.org/distribute_setup.py 33 | python distribute_setup.py 34 | easy_install pip 35 | 36 | **pyzmail** can be installed the old way from sources. Download the archive from 37 | `pypi `_ and extract its content 38 | into a directory. *cd* into this directory and run:: 39 | 40 | > cd pyzmail-X.X.X 41 | > python setup.py install 42 | 43 | Binary version of the scripts for **Windows** pyzmail-|release|-win32.zip can 44 | be downloaded from `here `__. 45 | 46 | **pyzmail** sources are also available on **github** 47 | `https://github.com/aspineux/pyzmail `_ 48 | 49 | Support for Python 3.x 50 | ---------------------- 51 | .. sidebar:: Python 3.2+ supported 52 | 53 | .. image:: /_static/python-3.png 54 | 55 | Python **3.2** is supported and has been tested. Python 3.0 and 3.1 are not supported 56 | because none of them provide functions to handle 8bits encoded emails like in **3.2** 57 | ( :py:func:`email.message_from_bytes` & :py:func:`email.message_from_binary_file` ) 58 | 59 | At installation time, **pyzmail** sources are automatically converted by 60 | `distribute `_ using **2to3**. 61 | 62 | Unfortunately, **scripts** are not converted in the process. You can convert them 63 | using **2to3** yourself *(adapt* **paths** *to fit you configuration)*:: 64 | 65 | /opt/python-3.2.2/bin/2to3 --no-diffs --write --nobackups /opt/python-3.2.2/bin/pyzinfomail 66 | /opt/python-3.2.2/bin/2to3 --no-diffs --write --nobackups /opt/python-3.2.2/bin/pyzsendmail 67 | 68 | 69 | Use pyzmail 70 | ----------- 71 | 72 | The package is split into 3 modules: 73 | 74 | * `generate `_: Useful functions to compose and send mail s 75 | * `parse `_: Useful functions to parse emails 76 | * `utils `_: Various functions used by other modules 77 | 78 | Most important functions are available from the top of the `pyzmail `_ package. 79 | 80 | usage sample:: 81 | 82 | import pyzmail 83 | 84 | #access function from top of pyzmail 85 | ret=pyzmail.compose_mail('me@foo.com', [ 'him@bar.com'], u'subject', \ 86 | 'iso-8859-1', ('Hello world', 'us-ascii')) 87 | payload=ret[0] 88 | print payload 89 | msg=pyzmail.PyzMessage.factory(payload) 90 | print msg.get_subject() 91 | 92 | #use more specific function from inside modules 93 | print pyzmail.generate.format_addresses([('John', 'john@foo.com') ], \ 94 | 'From', 'us-ascii') 95 | print pyzmail.parse.decode_mail_header('=?iso-8859-1?q?Hello?=') 96 | 97 | More in the `Quick Example`_ section. 98 | 99 | 100 | Documentation 101 | ------------- 102 | 103 | You can find lots of sample inside the *docstrings* but also in the *tests* 104 | directory. 105 | 106 | The documentation, samples, docstring and articles are all fitted for python 2.x. 107 | Some occasional hint give some tricks about Python 3.x. 108 | 109 | Articles 110 | ^^^^^^^^ 111 | 112 | To understand how this library works, you will find these 3 articles very useful. 113 | They have been written before the first release of **pyzmail** and the code has 114 | changed a little since: 115 | 116 | - `Parsing email using Python part 1 of 2 : The Header `_ 117 | - `Parsing email using Python part 2 of 2 : The content `_ 118 | - `Generate and send mail with python: tutorial `_ 119 | 120 | 121 | API documentation 122 | ^^^^^^^^^^^^^^^^^ 123 | 124 | The `API documentation `_ in *epydoc* format contains a lot 125 | of **samples** in *doctest* string. You will find them very useful too. 126 | 127 | 128 | Support 129 | ------- 130 | 131 | Ask your questions `here `__ 132 | 133 | Quick Example 134 | ------------- 135 | 136 | Lets show you how it works ! 137 | 138 | Compose an email 139 | ^^^^^^^^^^^^^^^^ 140 | 141 | :: 142 | 143 | import pyzmail 144 | 145 | sender=(u'Me', 'me@foo.com') 146 | recipients=[(u'Him', 'him@bar.com'), 'just@me.com'] 147 | subject=u'the subject' 148 | text_content=u'Bonjour aux Fran\xe7ais' 149 | prefered_encoding='iso-8859-1' 150 | text_encoding='iso-8859-1' 151 | 152 | payload, mail_from, rcpt_to, msg_id=pyzmail.compose_mail(\ 153 | sender, \ 154 | recipients, \ 155 | subject, \ 156 | prefered_encoding, \ 157 | (text_content, text_encoding), \ 158 | html=None, \ 159 | attachments=[('attached content', 'text', 'plain', 'text.txt', \ 160 | 'us-ascii')]) 161 | 162 | print payload 163 | 164 | Look a the output:: 165 | 166 | Content-Type: multipart/mixed; boundary="===============1727493275==" 167 | MIME-Version: 1.0 168 | From: Me 169 | To: Him , just@me.com 170 | Subject: the subject 171 | Date: Fri, 19 Aug 2011 16:04:42 +0200 172 | 173 | --===============1727493275== 174 | Content-Type: text/plain; charset="iso-8859-1" 175 | MIME-Version: 1.0 176 | Content-Transfer-Encoding: quoted-printable 177 | 178 | Bonjour aux Fran=E7ais 179 | --===============1727493275== 180 | Content-Type: text/plain; charset="us-ascii" 181 | MIME-Version: 1.0 182 | Content-Transfer-Encoding: 7bit 183 | Content-Disposition: attachment; filename="text.txt" 184 | 185 | attached content 186 | --===============1727493275==-- 187 | 188 | Send an email 189 | ^^^^^^^^^^^^^ 190 | 191 | First take a look at the other values returned by ``pyzmail.compose_mail()``:: 192 | 193 | print 'Sender address:', mail_from 194 | print 'Recipients:', rcpt_to 195 | 196 | Here are the values I can reuse for my SMTP connection:: 197 | 198 | Sender address: me@foo.com 199 | Recipients: ['him@bar.com', 'just@me.com'] 200 | 201 | I want to send my email via my Gmail account:: 202 | 203 | smtp_host='smtp.gmail.com' 204 | smtp_port=587 205 | smtp_mode='tls' 206 | smtp_login='my.gmail.addresse@gmail.com' 207 | smtp_password='my.gmail.password' 208 | 209 | ret=pyzmail.send_mail(payload, mail_from, rcpt_to, smtp_host, \ 210 | smtp_port=smtp_port, smtp_mode=smtp_mode, \ 211 | smtp_login=smtp_login, smtp_password=smtp_password) 212 | 213 | if isinstance(ret, dict): 214 | if ret: 215 | print 'failed recipients:', ', '.join(ret.keys()) 216 | else: 217 | print 'success' 218 | else: 219 | print 'error:', ret 220 | 221 | Here ``pyzmail.send_mail()`` combine **SSL** and **authentication**. 222 | 223 | 224 | Parse an email 225 | ^^^^^^^^^^^^^^ 226 | 227 | Now lets try to read the email we have just composed:: 228 | 229 | msg=pyzmail.PyzMessage.factory(payload) 230 | 231 | print 'Subject: %r' % (msg.get_subject(), ) 232 | print 'From: %r' % (msg.get_address('from'), ) 233 | print 'To: %r' % (msg.get_addresses('to'), ) 234 | print 'Cc: %r' % (msg.get_addresses('cc'), ) 235 | 236 | Take a look at the outpout:: 237 | 238 | Subject: u'the subject' 239 | From: (u'Me', 'me@foo.com') 240 | To: [(u'Him', 'him@bar.com'), (u'just@me.com', 'just@me.com')] 241 | Cc: [] 242 | 243 | 244 | And a little further regarding the mail content and attachment:: 245 | 246 | for mailpart in msg.mailparts: 247 | print ' %sfilename=%r alt_filename=%r type=%s charset=%s desc=%s size=%d' % ( \ 248 | '*'if mailpart.is_body else ' ', \ 249 | mailpart.filename, \ 250 | mailpart.sanitized_filename, \ 251 | mailpart.type, \ 252 | mailpart.charset, \ 253 | mailpart.part.get('Content-Description'), \ 254 | len(mailpart.get_payload()) ) 255 | if mailpart.type.startswith('text/'): 256 | # display first line of the text 257 | payload, used_charset=pyzmail.decode_text(mailpart.get_payload(), mailpart.charset, None) 258 | print ' >', payload.split('\\n')[0] 259 | 260 | And the output:: 261 | 262 | *filename=None alt_filename='text.txt' type=text/plain charset=iso-8859-1 desc=None size=20 263 | > Bonjour aux Français 264 | filename=u'text.txt' alt_filename='text-01.txt' type=text/plain charset=us-ascii desc=None size=16 265 | > attached content 266 | 267 | The first one, with a ***** is the *text* content, the second one is the attachment. 268 | 269 | You also have direct access to the *text* and *HTML* content using:: 270 | 271 | if msg.text_part!=None: 272 | print '-- text --' 273 | print msg.text_part.get_payload() 274 | 275 | if msg.html_part!=None: 276 | print '-- html --' 277 | print msg.html_part.get_payload() 278 | 279 | And the output:: 280 | 281 | -- text -- 282 | Bonjour aux Français 283 | 284 | Their is no *HTML* part ! 285 | 286 | Tricks 287 | ------ 288 | 289 | 290 | Embedding image in HTML email 291 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 292 | 293 | Image embedding differs from linked images in that the image itself is 294 | encoded, and included inside the message. Instead of using a normal URL 295 | in the *IMG* tag inside the HTML body, we must use a *cid:target* reference 296 | and assign this *target* name to the *Content-ID* of the embedded file. 297 | 298 | See this sample:: 299 | 300 | import base64 301 | import pyzmail 302 | 303 | angry_gif=base64.b64decode( 304 | """R0lGODlhDgAOALMAAAwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA//rMUf8A/wD/ 305 | //Tw5CH5BAAAAAAALAAAAAAOAA4AgwwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA 306 | //rMUf8A/wD///Tw5AQ28B1Gqz3S6jop2sxnAYNGaghAHirQUZh6sEDGPQgy5/b9UI+eZkAkghhG 307 | ZPLIbMKcDMwLhIkAADs= 308 | """) 309 | 310 | text_content=u"I'm very angry. See attached document." 311 | html_content=u'I\'m very angry. ' \ 312 | '.\n' \ 313 | 'See attached document.' 314 | 315 | payload, mail_from, rcpt_to, msg_id=pyzmail.compose_mail(\ 316 | (u'Me', 'me@foo.com'), \ 317 | [(u'Him', 'him@bar.com'), 'just@me.com'], \ 318 | u'the subject', \ 319 | 'iso-8859-1', \ 320 | (text_content, 'iso-8859-1'), \ 321 | (html_content, 'iso-8859-1'), \ 322 | attachments=[('The price of RAM modules is increasing.', \ 323 | 'text', 'plain', 'text.txt', 'us-ascii'), ], 324 | embeddeds=[(angry_gif, 'image', 'gif', 'angry_gif', None), ]) 325 | 326 | print payload 327 | 328 | And here is the *payload*:: 329 | 330 | Content-Type: multipart/mixed; boundary="===============1435507538==" 331 | MIME-Version: 1.0 332 | From: Me 333 | To: Him , just@me.com 334 | Subject: the subject 335 | Date: Fri, 02 Sep 2011 01:40:52 +0200 336 | 337 | --===============1435507538== 338 | Content-Type: multipart/related; boundary="===============0638818366==" 339 | MIME-Version: 1.0 340 | 341 | --===============0638818366== 342 | Content-Type: multipart/alternative; boundary="===============0288407648==" 343 | MIME-Version: 1.0 344 | 345 | --===============0288407648== 346 | Content-Type: text/plain; charset="iso-8859-1" 347 | MIME-Version: 1.0 348 | Content-Transfer-Encoding: quoted-printable 349 | 350 | I'm very angry. See attached document. 351 | --===============0288407648== 352 | Content-Type: text/html; charset="iso-8859-1" 353 | MIME-Version: 1.0 354 | Content-Transfer-Encoding: quoted-printable 355 | 356 | I'm very angry. . See attached doc= 357 | ument. 358 | --===============0288407648==-- 359 | --===============0638818366== 360 | Content-Type: image/gif 361 | MIME-Version: 1.0 362 | Content-Transfer-Encoding: base64 363 | Content-ID: 364 | Content-Disposition: inline 365 | 366 | R0lGODlhDgAOALMAAAwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA//rMUf8A/wD/ 367 | //Tw5CH5BAAAAAAALAAAAAAOAA4AgwwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA 368 | //rMUf8A/wD///Tw5AQ28B1Gqz3S6jop2sxnAYNGaghAHirQUZh6sEDGPQgy5/b9UI+eZkAkghhG 369 | ZPLIbMKcDMwLhIkAADs= 370 | --===============0638818366==-- 371 | --===============1435507538== 372 | Content-Type: text/plain; charset="us-ascii" 373 | MIME-Version: 1.0 374 | Content-Transfer-Encoding: 7bit 375 | Content-Disposition: attachment; filename="text.txt" 376 | 377 | The price of RAM module is increasing. 378 | --===============1435507538==-- 379 | 380 | 381 | Scripts 382 | ------- 383 | 384 | Binary executables for Windows of these script are available in 385 | the `Download`_ section below. 386 | 387 | pyzsendmail 388 | ^^^^^^^^^^^ 389 | 390 | **pyzsendmail** is a command line script to compose and send simple and complex emails. 391 | 392 | Features: 393 | 394 | - **SSL**, **TLS** , **authentication** 395 | - **HTML** content and *embedded images* 396 | - **attachments** 397 | - *Internationalisation* 398 | 399 | Read the :doc:`manual ` for more. 400 | 401 | Under *Windows* **pyzsendmail.exe** can replace the now old `blat.exe `_ and 402 | `bmail.exe `_. 403 | 404 | 405 | pyzinfomail 406 | ^^^^^^^^^^^ 407 | 408 | **pyzinfomail** is a command line script reading an email 409 | from a file and printing most important information. Mostly to show how to use 410 | **pyzmail** library. Read the :doc:`manual ` for more. 411 | 412 | License 413 | ------- 414 | 415 | **pyzmail** iis released under the GNU Lesser General Public License ( LGPL ). 416 | 417 | Links 418 | ----- 419 | 420 | More links about parsing and writing mail in python 421 | 422 | - `formataddr() and unicode `_ 423 | - `Sending Unicode emails in Python `_ 424 | - `Sending Email with smtplib `_ 425 | 426 | 427 | .. 428 | Not used yet 429 | Contents: 430 | 431 | .. toctree:: 432 | :maxdepth: 2 433 | 434 | man/pyzsendmail 435 | 436 | 437 | Indices and tables 438 | ================== 439 | 440 | * :ref:`genindex` 441 | * :ref:`modindex` 442 | * :ref:`search` 443 | 444 | -------------------------------------------------------------------------------- /pyzmail/docs/source/man/pyzinfomail.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | pyzinfomail manual page 4 | ======================= 5 | 6 | Synopsis 7 | -------- 8 | 9 | **pyzinfomail** <*filename*> 10 | 11 | Description 12 | ----------- 13 | 14 | **pyzinfomail** parse and display some data from an email. This is mostly a 15 | sample on how to use the **pyzmail** library. 16 | 17 | 18 | Sample 19 | ------ 20 | 21 | usage:: 22 | 23 | $ pyzinfomail mail.eml 24 | Subject: u'The subject' 25 | From: (u'Sender', 'sender@example.com') 26 | To: [(u'Recipient', 'recipient@example.com')] 27 | Cc: [] 28 | Date: 'Tue, 7 Jun 2011 16:32:17 +0200' 29 | Message-Id: '20110830190805.5096.33348.pyzsendmail@host.example.com' 30 | *filename=None type=text/plain charset=ISO-8859-1 desc=None size=13 31 | > Hello World 32 | *filename=None type=text/html charset=ISO-8859-1 desc=None size=23 33 | 34 | 35 | See also 36 | -------- 37 | 38 | :manpage:`pyzsendmail(1)` 39 | 40 | Author 41 | ------ 42 | 43 | Alain Spineux 44 | -------------------------------------------------------------------------------- /pyzmail/docs/source/man/pyzsendmail.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | pyzsendmail manual page 4 | ======================= 5 | 6 | Synopsis 7 | -------- 8 | 9 | **pyzsendmail** [*options*] 10 | 11 | Description 12 | ----------- 13 | 14 | **pyzsendmail** compose and send mails. 15 | 16 | Compose an email including *text* and/or *HTML* content, add *attachments* 17 | of any kind, or *embed* images in the HTML content. Depending the need, 18 | **pyzsendmail** adapts the MIME structure of the email. 19 | 20 | **pyzsendmail** handle SSL, TLS and authentication. 21 | 22 | **pyzsendmail** can send to one or multiple recipients, but also support *CC* 23 | and *BCC* recipients. 24 | 25 | 26 | Options 27 | ------- 28 | 29 | .. program:: pyzsendmail 30 | 31 | .. option:: -h, --help 32 | 33 | show this help message and exit 34 | 35 | .. option:: -H name_or_ip, --smtp-host=name_or_ip 36 | 37 | SMTP host relay 38 | 39 | .. option:: -p port, --smtp-port=port 40 | 41 | SMTP port (default=25) 42 | 43 | .. option:: -L login, --smtp-login=login 44 | 45 | SMTP login (if authentication is required) 46 | 47 | .. option:: -P password, --smtp-password=password 48 | 49 | SMTP password (if authentication is required) 50 | 51 | .. option:: -m mode, --smtp-mode=mode 52 | 53 | smtp mode in 'normal', 'ssl', 'tls'. (default='normal') 54 | 55 | .. option:: -A charset, --arg-charset=charset 56 | 57 | command line arguments charset (default=) 58 | 59 | .. option:: -C charset, --mail-charset=charset 60 | 61 | mail default charset (default=) 62 | 63 | .. option:: -f sender, --from=sender 64 | 65 | sender address 66 | 67 | .. option:: -t recipient, --to=recipient 68 | 69 | add one recipient address 70 | 71 | .. option:: -c recipient, --cc=recipient 72 | 73 | add one CC address 74 | 75 | .. option:: -b recipient, --bcc=recipient 76 | 77 | add one BCC address 78 | 79 | .. option:: -s subject, --subject=subject 80 | 81 | message subject 82 | 83 | .. option:: -T text, --text=text 84 | 85 | text content in the form 86 | [text_charset]:@filename 87 | or 88 | [text_charset]:"litteral content" 89 | 90 | .. option:: -M html, --html=html 91 | 92 | html content in the form 93 | [text_charset]:@filename 94 | or 95 | [text_charset]:"literal content" 96 | 97 | .. option:: -a file, --attach=file 98 | 99 | add an attachment in the form: 100 | maintype/subtype:filename:target_file[:text_charset] 101 | for example 102 | image/jpg:picture.jpg:thepicture.png 103 | or 104 | image/jpg:picture.jpg:C:\\thepicture.png: 105 | (notice the trailing ':' to disambiguate the : of the drive letter) 106 | or 107 | text/plain:file.txt:C:\\report.txt:windows-1252 108 | 109 | .. option:: -e file, --embed=file 110 | 111 | add embedded data in the form: 112 | maintype/subtype:content-id:target_file[:text_charset] 113 | for example 114 | image/jpg:picture:thepicture.png 115 | 116 | .. option:: -E, --eicar 117 | 118 | include eicar virus in attachments, for testing Anti-virus 119 | 120 | Arguments 121 | --------- 122 | 123 | **login** and **password** must be *utf-8* encoded if they contains non *us-ascii* 124 | characters. 125 | 126 | **address** can be of the form: 127 | \"Foo Bar \" 128 | 129 | **or** 130 | 131 | \"foo.bar\@example.com\" 132 | 133 | Name can contain non us-ascii characters. They are supposed to use the 134 | command line charset encoding. 135 | 136 | **text** and **HTML** content can be in the *literal* form: 137 | - :\"The text content\" 138 | - utf8:\"The text content\" 139 | 140 | In both samples, the content is encoded using the *encoding* of the 141 | command line argument. In first sample, notice the **:** at beginning, 142 | the content will be encoded using the mail default charset, 143 | in the second sample, the text will be re-encoded into utf8. 144 | 145 | **or** using content of a *file*: 146 | 147 | - :@"C:\\file.txt" 148 | - windows-1252:@"C:\\file.txt" 149 | 150 | In first sample, notice the **:** at beginning, the file is supposed to be 151 | encoded using mail default charset and will be encoded this way in the 152 | email. In second sample, the file is supposed to be encoded using 153 | *windows-1252* charset and will be encoded this way in the email. 154 | 155 | **attachment** and **embedded** files 156 | In attachment and embedded content, the *text_charset* is used only if the 157 | *maintype* is \'text\'. The file is supposed to be encoded using 158 | *text_charset* and will be encoded using this charset in the email. 159 | 160 | Samples 161 | ------- 162 | 163 | :: 164 | 165 | pyzsendmail -H localhost -p 25 -f "Me " -t "foo\@example.com" -t "Bar 177 | 178 | -------------------------------------------------------------------------------- /pyzmail/epydoc.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* Epydoc CSS Stylesheet 4 | * 5 | * This stylesheet can be used to customize the appearance of epydoc's 6 | * HTML output. 7 | * 8 | */ 9 | 10 | /* Default Colors & Styles 11 | * - Set the default foreground & background color with 'body'; and 12 | * link colors with 'a:link' and 'a:visited'. 13 | * - Use bold for decision list terms. 14 | * - The heading styles defined here are used for headings *within* 15 | * docstring descriptions. All headings used by epydoc itself use 16 | * either class='epydoc' or class='toc' (CSS styles for both 17 | * defined below). 18 | */ 19 | body { background: #ffffff; color: #000000; } 20 | p { margin-top: 0.5em; margin-bottom: 0.5em; } 21 | a:link { color: #0000ff; } 22 | a:visited { color: #204080; } 23 | dt { font-weight: bold; } 24 | h1 { font-size: +140%; font-style: italic; 25 | font-weight: bold; } 26 | h2 { font-size: +125%; font-style: italic; 27 | font-weight: bold; } 28 | h3 { font-size: +110%; font-style: italic; 29 | font-weight: normal; } 30 | code { font-size: 100%; } 31 | /* N.B.: class, not pseudoclass */ 32 | a.link { font-family: monospace; } 33 | 34 | /* Page Header & Footer 35 | * - The standard page header consists of a navigation bar (with 36 | * pointers to standard pages such as 'home' and 'trees'); a 37 | * breadcrumbs list, which can be used to navigate to containing 38 | * classes or modules; options links, to show/hide private 39 | * variables and to show/hide frames; and a page title (using 40 | *

). The page title may be followed by a link to the 41 | * corresponding source code (using 'span.codelink'). 42 | * - The footer consists of a navigation bar, a timestamp, and a 43 | * pointer to epydoc's homepage. 44 | */ 45 | h1.epydoc { margin: 0; font-size: +140%; font-weight: bold; } 46 | h2.epydoc { font-size: +130%; font-weight: bold; } 47 | h3.epydoc { font-size: +115%; font-weight: bold; 48 | margin-top: 0.2em; } 49 | td h3.epydoc { font-size: +115%; font-weight: bold; 50 | margin-bottom: 0; } 51 | table.navbar { background: #a0c0ff; color: #000000; 52 | border: 2px groove #c0d0d0; } 53 | table.navbar table { color: #000000; } 54 | th.navbar-select { background: #70b0ff; 55 | color: #000000; } 56 | table.navbar a { text-decoration: none; } 57 | table.navbar a:link { color: #0000ff; } 58 | table.navbar a:visited { color: #204080; } 59 | span.breadcrumbs { font-size: 85%; font-weight: bold; } 60 | span.options { font-size: 70%; } 61 | span.codelink { font-size: 85%; } 62 | td.footer { font-size: 85%; } 63 | 64 | /* Table Headers 65 | * - Each summary table and details section begins with a 'header' 66 | * row. This row contains a section title (marked by 67 | * 'span.table-header') as well as a show/hide private link 68 | * (marked by 'span.options', defined above). 69 | * - Summary tables that contain user-defined groups mark those 70 | * groups using 'group header' rows. 71 | */ 72 | td.table-header { background: #70b0ff; color: #000000; 73 | border: 1px solid #608090; } 74 | td.table-header table { color: #000000; } 75 | td.table-header table a:link { color: #0000ff; } 76 | td.table-header table a:visited { color: #204080; } 77 | span.table-header { font-size: 120%; font-weight: bold; } 78 | th.group-header { background: #c0e0f8; color: #000000; 79 | text-align: left; font-style: italic; 80 | font-size: 115%; 81 | border: 1px solid #608090; } 82 | 83 | /* Summary Tables (functions, variables, etc) 84 | * - Each object is described by a single row of the table with 85 | * two cells. The left cell gives the object's type, and is 86 | * marked with 'code.summary-type'. The right cell gives the 87 | * object's name and a summary description. 88 | * - CSS styles for the table's header and group headers are 89 | * defined above, under 'Table Headers' 90 | */ 91 | table.summary { border-collapse: collapse; 92 | background: #e8f0f8; color: #000000; 93 | border: 1px solid #608090; 94 | margin-bottom: 0.5em; } 95 | td.summary { border: 1px solid #608090; } 96 | code.summary-type { font-size: 85%; } 97 | table.summary a:link { color: #0000ff; } 98 | table.summary a:visited { color: #204080; } 99 | 100 | 101 | /* Details Tables (functions, variables, etc) 102 | * - Each object is described in its own div. 103 | * - A single-row summary table w/ table-header is used as 104 | * a header for each details section (CSS style for table-header 105 | * is defined above, under 'Table Headers'). 106 | */ 107 | table.details { border-collapse: collapse; 108 | background: #e8f0f8; color: #000000; 109 | border: 1px solid #608090; 110 | margin: .2em 0 0 0; } 111 | table.details table { color: #000000; } 112 | table.details a:link { color: #0000ff; } 113 | table.details a:visited { color: #204080; } 114 | 115 | /* Fields */ 116 | dl.fields { margin-left: 2em; margin-top: 1em; 117 | margin-bottom: 1em; } 118 | dl.fields dd ul { margin-left: 0em; padding-left: 0em; } 119 | dl.fields dd ul li ul { margin-left: 2em; padding-left: 0em; } 120 | div.fields { margin-left: 2em; } 121 | div.fields p { margin-bottom: 0.5em; } 122 | 123 | /* Index tables (identifier index, term index, etc) 124 | * - link-index is used for indices containing lists of links 125 | * (namely, the identifier index & term index). 126 | * - index-where is used in link indices for the text indicating 127 | * the container/source for each link. 128 | * - metadata-index is used for indices containing metadata 129 | * extracted from fields (namely, the bug index & todo index). 130 | */ 131 | table.link-index { border-collapse: collapse; 132 | background: #e8f0f8; color: #000000; 133 | border: 1px solid #608090; } 134 | td.link-index { border-width: 0px; } 135 | table.link-index a:link { color: #0000ff; } 136 | table.link-index a:visited { color: #204080; } 137 | span.index-where { font-size: 70%; } 138 | table.metadata-index { border-collapse: collapse; 139 | background: #e8f0f8; color: #000000; 140 | border: 1px solid #608090; 141 | margin: .2em 0 0 0; } 142 | td.metadata-index { border-width: 1px; border-style: solid; } 143 | table.metadata-index a:link { color: #0000ff; } 144 | table.metadata-index a:visited { color: #204080; } 145 | 146 | /* Function signatures 147 | * - sig* is used for the signature in the details section. 148 | * - .summary-sig* is used for the signature in the summary 149 | * table, and when listing property accessor functions. 150 | * */ 151 | .sig-name { color: #006080; } 152 | .sig-arg { color: #008060; } 153 | .sig-default { color: #602000; } 154 | .summary-sig { font-family: monospace; } 155 | .summary-sig-name { color: #006080; font-weight: bold; } 156 | table.summary a.summary-sig-name:link 157 | { color: #006080; font-weight: bold; } 158 | table.summary a.summary-sig-name:visited 159 | { color: #006080; font-weight: bold; } 160 | .summary-sig-arg { color: #006040; } 161 | .summary-sig-default { color: #501800; } 162 | 163 | /* Subclass list 164 | */ 165 | ul.subclass-list { display: inline; } 166 | ul.subclass-list li { display: inline; } 167 | 168 | /* To render variables, classes etc. like functions */ 169 | table.summary .summary-name { color: #006080; font-weight: bold; 170 | font-family: monospace; } 171 | table.summary 172 | a.summary-name:link { color: #006080; font-weight: bold; 173 | font-family: monospace; } 174 | table.summary 175 | a.summary-name:visited { color: #006080; font-weight: bold; 176 | font-family: monospace; } 177 | 178 | /* Variable values 179 | * - In the 'variable details' sections, each varaible's value is 180 | * listed in a 'pre.variable' box. The width of this box is 181 | * restricted to 80 chars; if the value's repr is longer than 182 | * this it will be wrapped, using a backslash marked with 183 | * class 'variable-linewrap'. If the value's repr is longer 184 | * than 3 lines, the rest will be ellided; and an ellipsis 185 | * marker ('...' marked with 'variable-ellipsis') will be used. 186 | * - If the value is a string, its quote marks will be marked 187 | * with 'variable-quote'. 188 | * - If the variable is a regexp, it is syntax-highlighted using 189 | * the re* CSS classes. 190 | */ 191 | pre.variable { padding: .5em; margin: 0; 192 | background: #dce4ec; color: #000000; 193 | border: 1px solid #708890; } 194 | .variable-linewrap { color: #604000; font-weight: bold; } 195 | .variable-ellipsis { color: #604000; font-weight: bold; } 196 | .variable-quote { color: #604000; font-weight: bold; } 197 | .variable-group { color: #008000; font-weight: bold; } 198 | .variable-op { color: #604000; font-weight: bold; } 199 | .variable-string { color: #006030; } 200 | .variable-unknown { color: #a00000; font-weight: bold; } 201 | .re { color: #000000; } 202 | .re-char { color: #006030; } 203 | .re-op { color: #600000; } 204 | .re-group { color: #003060; } 205 | .re-ref { color: #404040; } 206 | 207 | /* Base tree 208 | * - Used by class pages to display the base class hierarchy. 209 | */ 210 | pre.base-tree { font-size: 80%; margin: 0; } 211 | 212 | /* Frames-based table of contents headers 213 | * - Consists of two frames: one for selecting modules; and 214 | * the other listing the contents of the selected module. 215 | * - h1.toc is used for each frame's heading 216 | * - h2.toc is used for subheadings within each frame. 217 | */ 218 | h1.toc { text-align: center; font-size: 105%; 219 | margin: 0; font-weight: bold; 220 | padding: 0; } 221 | h2.toc { font-size: 100%; font-weight: bold; 222 | margin: 0.5em 0 0 -0.3em; } 223 | 224 | /* Syntax Highlighting for Source Code 225 | * - doctest examples are displayed in a 'pre.py-doctest' block. 226 | * If the example is in a details table entry, then it will use 227 | * the colors specified by the 'table pre.py-doctest' line. 228 | * - Source code listings are displayed in a 'pre.py-src' block. 229 | * Each line is marked with 'span.py-line' (used to draw a line 230 | * down the left margin, separating the code from the line 231 | * numbers). Line numbers are displayed with 'span.py-lineno'. 232 | * The expand/collapse block toggle button is displayed with 233 | * 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not 234 | * modify the font size of the text.) 235 | * - If a source code page is opened with an anchor, then the 236 | * corresponding code block will be highlighted. The code 237 | * block's header is highlighted with 'py-highlight-hdr'; and 238 | * the code block's body is highlighted with 'py-highlight'. 239 | * - The remaining py-* classes are used to perform syntax 240 | * highlighting (py-string for string literals, py-name for names, 241 | * etc.) 242 | */ 243 | pre.py-doctest { padding: .5em; margin: 1em; 244 | background: #e8f0f8; color: #000000; 245 | border: 1px solid #708890; 246 | /* ASX */ 247 | white-space: pre-wrap; /* css-3 */ 248 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 249 | white-space: -pre-wrap; /* Opera 4-6 */ 250 | white-space: -o-pre-wrap; /* Opera 7 */ 251 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 252 | 253 | } 254 | table pre.py-doctest { background: #dce4ec; 255 | color: #000000; } 256 | pre.py-src { border: 2px solid #000000; 257 | background: #f0f0f0; color: #000000; } 258 | .py-line { border-left: 2px solid #000000; 259 | margin-left: .2em; padding-left: .4em; } 260 | .py-lineno { font-style: italic; font-size: 90%; 261 | padding-left: .5em; } 262 | a.py-toggle { text-decoration: none; } 263 | div.py-highlight-hdr { border-top: 2px solid #000000; 264 | border-bottom: 2px solid #000000; 265 | background: #d8e8e8; } 266 | div.py-highlight { border-bottom: 2px solid #000000; 267 | background: #d0e0e0; } 268 | .py-prompt { color: #005050; font-weight: bold;} 269 | .py-more { color: #005050; font-weight: bold;} 270 | .py-string { color: #006030; } 271 | .py-comment { color: #003060; } 272 | .py-keyword { color: #600000; } 273 | .py-output { color: #404040; } 274 | .py-name { color: #000050; } 275 | .py-name:link { color: #000050 !important; } 276 | .py-name:visited { color: #000050 !important; } 277 | .py-number { color: #005000; } 278 | .py-defname { color: #000060; font-weight: bold; } 279 | .py-def-name { color: #000060; font-weight: bold; } 280 | .py-base-class { color: #000060; } 281 | .py-param { color: #000060; } 282 | .py-docstring { color: #006030; } 283 | .py-decorator { color: #804020; } 284 | /* Use this if you don't want links to names underlined: */ 285 | /*a.py-name { text-decoration: none; }*/ 286 | 287 | /* Graphs & Diagrams 288 | * - These CSS styles are used for graphs & diagrams generated using 289 | * Graphviz dot. 'img.graph-without-title' is used for bare 290 | * diagrams (to remove the border created by making the image 291 | * clickable). 292 | */ 293 | img.graph-without-title { border: none; } 294 | img.graph-with-title { border: 1px solid #000000; } 295 | span.graph-title { font-weight: bold; } 296 | span.graph-caption { } 297 | 298 | /* General-purpose classes 299 | * - 'p.indent-wrapped-lines' defines a paragraph whose first line 300 | * is not indented, but whose subsequent lines are. 301 | * - The 'nomargin-top' class is used to remove the top margin (e.g. 302 | * from lists). The 'nomargin' class is used to remove both the 303 | * top and bottom margin (but not the left or right margin -- 304 | * for lists, that would cause the bullets to disappear.) 305 | */ 306 | p.indent-wrapped-lines { padding: 0 0 0 7em; text-indent: -7em; 307 | margin: 0; } 308 | .nomargin-top { margin-top: 0; } 309 | .nomargin { margin-top: 0; margin-bottom: 0; } 310 | 311 | /* HTML Log */ 312 | div.log-block { padding: 0; margin: .5em 0 .5em 0; 313 | background: #e8f0f8; color: #000000; 314 | border: 1px solid #000000; } 315 | div.log-error { padding: .1em .3em .1em .3em; margin: 4px; 316 | background: #ffb0b0; color: #000000; 317 | border: 1px solid #000000; } 318 | div.log-warning { padding: .1em .3em .1em .3em; margin: 4px; 319 | background: #ffffb0; color: #000000; 320 | border: 1px solid #000000; } 321 | div.log-info { padding: .1em .3em .1em .3em; margin: 4px; 322 | background: #b0ffb0; color: #000000; 323 | border: 1px solid #000000; } 324 | h2.log-hdr { background: #70b0ff; color: #000000; 325 | margin: 0; padding: 0em 0.5em 0em 0.5em; 326 | border-bottom: 1px solid #000000; font-size: 110%; } 327 | p.log { font-weight: bold; margin: .5em 0 .5em 0; } 328 | tr.opt-changed { color: #000000; font-weight: bold; } 329 | tr.opt-default { color: #606060; } 330 | pre.log { margin: 0; padding: 0; padding-left: 1em; } 331 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # pyzmail/__init__.py 3 | # (c) Alain Spineux 4 | # http://www.magiksys.net/pyzmail 5 | # Released under LGPL 6 | 7 | import utils 8 | from generate import compose_mail, send_mail, send_mail2 9 | from parse import email_address_re, PyzMessage, PzMessage, decode_text 10 | from parse import message_from_string, message_from_file 11 | from parse import message_from_bytes, message_from_binary_file # python >= 3.2 12 | from version import __version__ 13 | 14 | # to help epydoc to display functions available from top of the package 15 | __all__= [ 'compose_mail', 'send_mail', 'send_mail2', 'email_address_re', 16 | 'PyzMessage', 'PzMessage', 'decode_text', '__version__', 17 | 'utils', 'generate', 'parse', 'version', 18 | 'message_from_string','message_from_file', 19 | 'message_from_binary_file', 'message_from_bytes', # python >= 3.2 20 | ] 21 | 22 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/generate.py: -------------------------------------------------------------------------------- 1 | # 2 | # pyzmail/generate.py 3 | # (c) Alain Spineux 4 | # http://www.magiksys.net/pyzmail 5 | # Released under LGPL 6 | 7 | """ 8 | Useful functions to compose and send emails. 9 | 10 | For short: 11 | 12 | >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'), 13 | ... [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'), 14 | ... attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 15 | ... #doctest: +SKIP 16 | >>> error=send_mail(payload, mail_from, rcpt_to, 'localhost', smtp_port=25) 17 | ... #doctest: +SKIP 18 | """ 19 | 20 | import os, sys 21 | import time 22 | import base64 23 | import smtplib, socket 24 | import email 25 | import email.encoders 26 | import email.header 27 | import email.utils 28 | import email.mime 29 | import email.mime.base 30 | import email.mime.text 31 | import email.mime.image 32 | import email.mime.multipart 33 | 34 | import utils 35 | 36 | def format_addresses(addresses, header_name=None, charset=None): 37 | """ 38 | Convert a list of addresses into a MIME-compliant header for a From, To, Cc, 39 | or any other I{address} related field. 40 | This mixes the use of email.utils.formataddr() and email.header.Header(). 41 | 42 | @type addresses: list 43 | @param addresses: list of addresses, can be a mix of string a tuple of the form 44 | C{[ 'address@domain', (u'Name', 'name@domain'), ...]}. 45 | If C{u'Name'} contains non us-ascii characters, it must be a 46 | unicode string or encoded using the I{charset} argument. 47 | @type header_name: string or None 48 | @keyword header_name: the name of the header. Its length is used to limit 49 | the length of the first line of the header according the RFC's 50 | requirements. (not very important, but it's better to match the 51 | requirements when possible) 52 | @type charset: str 53 | @keyword charset: the encoding charset for non unicode I{name} and a B{hint} 54 | for encoding of unicode string. In other words, 55 | if the I{name} of an address in a byte string containing non 56 | I{us-ascii} characters, then C{name.decode(charset)} 57 | must generate the expected result. If a unicode string 58 | is used instead, charset will be tried to encode the 59 | string, if it fail, I{utf-8} will be used. 60 | With B{Python 3.x} I{charset} is no more a hint and an exception will 61 | be raised instead of using I{utf-8} has a fall back. 62 | @rtype: str 63 | @return: the encoded list of formated addresses separated by commas, 64 | ready to use as I{Header} value. 65 | 66 | >>> print format_addresses([('John', 'john@foo.com') ], 'From', 'us-ascii').encode() 67 | John 68 | >>> print format_addresses([(u'l\\xe9o', 'leo@foo.com') ], 'To', 'iso-8859-1').encode() 69 | =?iso-8859-1?q?l=E9o?= 70 | >>> print format_addresses([(u'l\\xe9o', 'leo@foo.com') ], 'To', 'us-ascii').encode() 71 | ... # don't work in 3.X because charset is more than a hint 72 | ... #doctest: +SKIP 73 | =?utf-8?q?l=C3=A9o?= 74 | >>> # because u'l\xe9o' cannot be encoded into us-ascii, utf8 is used instead 75 | >>> print format_addresses([('No\\xe9', 'noe@f.com'), (u'M\u0101ori', 'maori@b.com') ], 'Cc', 'iso-8859-1').encode() 76 | ... # don't work in 3.X because charset is more than a hint 77 | ... #doctest: +SKIP 78 | =?iso-8859-1?q?No=E9?= , =?utf-8?b?TcSBb3Jp?= 79 | >>> # 'No\xe9' is already encoded into iso-8859-1, but u'M\u0101ori' cannot be encoded into iso-8859-1 80 | >>> # then utf8 is used here 81 | >>> print format_addresses(['a@bar.com', ('John', 'john@foo.com') ], 'From', 'us-ascii').encode() 82 | a@bar.com , John 83 | """ 84 | header=email.header.Header(charset=charset, header_name=header_name) 85 | for i, address in enumerate(addresses): 86 | if i!=0: 87 | # add separator between addresses 88 | header.append(',', charset='us-ascii') 89 | 90 | try: 91 | name, addr=address 92 | except ValueError: 93 | # address is not a tuple, their is no name, only email address 94 | header.append(address, charset='us-ascii') 95 | else: 96 | # check if address name is a unicode or byte string in "pure" us-ascii 97 | if utils.is_usascii(name): 98 | # name is a us-ascii byte string, i can use formataddr 99 | formated_addr=email.utils.formataddr((name, addr)) 100 | # us-ascii must be used and not default 'charset' 101 | header.append(formated_addr, charset='us-ascii') 102 | else: 103 | # this is not as "pure" us-ascii string 104 | # Header will use "RFC2047" to encode the address name 105 | # if name is byte string, charset will be used to decode it first 106 | header.append(name) 107 | # here us-ascii must be used and not default 'charset' 108 | header.append('<%s>' % (addr,), charset='us-ascii') 109 | 110 | return header 111 | 112 | 113 | def build_mail(text, html=None, attachments=[], embeddeds=[]): 114 | """ 115 | Generate the core of the email message regarding the parameters. 116 | The structure of the MIME email may vary, but the general one is as follow:: 117 | 118 | multipart/mixed (only if attachments are included) 119 | | 120 | +-- multipart/related (only if embedded contents are included) 121 | | | 122 | | +-- multipart/alternative (only if text AND html are available) 123 | | | | 124 | | | +-- text/plain (text version of the message) 125 | | | +-- text/html (html version of the message) 126 | | | 127 | | +-- image/gif (where to include embedded contents) 128 | | 129 | +-- application/msword (where to add attachments) 130 | 131 | @param text: the text version of the message, under the form of a tuple: 132 | C{(encoded_content, encoding)} where I{encoded_content} is a byte string 133 | encoded using I{encoding}. 134 | I{text} can be None if the message has no text version. 135 | @type text: tuple or None 136 | @keyword html: the HTML version of the message, under the form of a tuple: 137 | C{(encoded_content, encoding)} where I{encoded_content} is a byte string 138 | encoded using I{encoding} 139 | I{html} can be None if the message has no HTML version. 140 | @type html: tuple or None 141 | @keyword attachments: the list of attachments to include into the mail, in the 142 | form [(data, maintype, subtype, filename, charset), ..] where : 143 | - I{data} : is the raw data, or a I{charset} encoded string for 'text' 144 | content. 145 | - I{maintype} : is a MIME main type like : 'text', 'image', 'application' .... 146 | - I{subtype} : is a MIME sub type of the above I{maintype} for example : 147 | 'plain', 'png', 'msword' for respectively 'text/plain', 'image/png', 148 | 'application/msword'. 149 | - I{filename} this is the filename of the attachment, it must be a 150 | 'us-ascii' string or a tuple of the form 151 | C{(encoding, language, encoded_filename)} 152 | following the RFC2231 requirement, for example 153 | C{('iso-8859-1', 'fr', u'r\\xe9pertoir.png'.encode('iso-8859-1'))} 154 | - I{charset} : if I{maintype} is 'text', then I{data} must be encoded 155 | using this I{charset}. It can be None for non 'text' content. 156 | @type attachments: list 157 | @keyword embeddeds: is a list of documents embedded inside the HTML or text 158 | version of the message. It is similar to the I{attachments} list, 159 | but I{filename} is replaced by I{content_id} that is related to 160 | the B{cid} reference into the HTML or text version of the message. 161 | @type embeddeds: list 162 | @rtype: inherit from email.Message 163 | @return: the message in a MIME object 164 | 165 | >>> mail=build_mail(('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 166 | >>> mail.set_boundary('===limit1==') 167 | >>> print mail.as_string(unixfrom=False) 168 | Content-Type: multipart/mixed; boundary="===limit1==" 169 | MIME-Version: 1.0 170 | 171 | --===limit1== 172 | Content-Type: text/plain; charset="us-ascii" 173 | MIME-Version: 1.0 174 | Content-Transfer-Encoding: 7bit 175 | 176 | Hello world 177 | --===limit1== 178 | Content-Type: text/plain; charset="us-ascii" 179 | MIME-Version: 1.0 180 | Content-Transfer-Encoding: 7bit 181 | Content-Disposition: attachment; filename="text.txt" 182 | 183 | attached 184 | --===limit1==-- 185 | 186 | """ 187 | 188 | main=text_part=html_part=None 189 | if text: 190 | content, charset=text 191 | main=text_part=email.mime.text.MIMEText(content, 'plain', charset) 192 | 193 | if html: 194 | content, charset=html 195 | main=html_part=email.mime.text.MIMEText(content, 'html', charset) 196 | 197 | if not text_part and not html_part: 198 | main=text_part=email.mime.text.MIMEText('', 'plain', 'us-ascii') 199 | elif text_part and html_part: 200 | # need to create a multipart/alternative to include text and html version 201 | main=email.mime.multipart.MIMEMultipart('alternative', None, [text_part, html_part]) 202 | 203 | if embeddeds: 204 | related=email.mime.multipart.MIMEMultipart('related') 205 | related.attach(main) 206 | for part in embeddeds: 207 | if not isinstance(part, email.mime.base.MIMEBase): 208 | data, maintype, subtype, content_id, charset=part 209 | if (maintype=='text'): 210 | part=email.mime.text.MIMEText(data, subtype, charset) 211 | else: 212 | part=email.mime.base.MIMEBase(maintype, subtype) 213 | part.set_payload(data) 214 | email.encoders.encode_base64(part) 215 | part.add_header('Content-ID', '<'+content_id+'>') 216 | part.add_header('Content-Disposition', 'inline') 217 | related.attach(part) 218 | main=related 219 | 220 | if attachments: 221 | mixed=email.mime.multipart.MIMEMultipart('mixed') 222 | mixed.attach(main) 223 | for part in attachments: 224 | if not isinstance(part, email.mime.base.MIMEBase): 225 | data, maintype, subtype, filename, charset=part 226 | if (maintype=='text'): 227 | part=email.mime.text.MIMEText(data, subtype, charset) 228 | else: 229 | part=email.mime.base.MIMEBase(maintype, subtype) 230 | part.set_payload(data) 231 | email.encoders.encode_base64(part) 232 | part.add_header('Content-Disposition', 'attachment', filename=filename) 233 | mixed.attach(part) 234 | main=mixed 235 | 236 | return main 237 | 238 | def complete_mail(message, sender, recipients, subject, default_charset, cc=[], bcc=[], message_id_string=None, date=None, headers=[]): 239 | """ 240 | Fill in the From, To, Cc, Subject, Date and Message-Id I{headers} of 241 | one existing message regarding the parameters. 242 | 243 | @type message:email.Message 244 | @param message: the message to fill in 245 | @type sender: tuple 246 | @param sender: a tuple of the form (u'Sender Name', 'sender.address@domain.com') 247 | @type recipients: list 248 | @param recipients: a list of addresses. Address can be tuple or string like 249 | expected by L{format_addresses()}, for example: C{[ 'address@dmain.com', 250 | (u'Recipient Name', 'recipient.address@domain.com'), ... ]} 251 | @type subject: str 252 | @param subject: The subject of the message, can be a unicode string or a 253 | string encoded using I{default_charset} encoding. Prefert unicode to 254 | byte string here. 255 | @type default_charset: str 256 | @param default_charset: The default charset for this email. Arguments 257 | that are non unicode string are supposed to be encoded using this charset. 258 | This I{charset} will be used has an hint when encoding mail content. 259 | @type cc: list 260 | @keyword cc: The I{carbone copy} addresses. Same format as the I{recipients} 261 | argument. 262 | @type bcc: list 263 | @keyword bcc: The I{blind carbone copy} addresses. Same format as the I{recipients} 264 | argument. 265 | @type message_id_string: str or None 266 | @keyword message_id_string: if None, don't append any I{Message-ID} to the 267 | mail, let the SMTP do the job, else use the string to generate a unique 268 | I{ID} using C{email.utils.make_msgid()}. The generated value is 269 | returned as last argument. For example use the name of your application. 270 | @type date: int or None 271 | @keyword date: utc time in second from the epoch or None. If None then 272 | use curent time C{time.time()} instead. 273 | @type headers: list of tuple 274 | @keyword headers: a list of C{(field, value)} tuples to fill in the mail 275 | header fields. values can be instances of email.header.Header or unicode strings 276 | that will be encoded using I{default_charset}. 277 | @rtype: tuple 278 | @return: B{(payload, mail_from, rcpt_to, msg_id)} 279 | - I{payload} (str) is the content of the email, generated from the message 280 | - I{mail_from} (str) is the address of the sender to pass to the SMTP host 281 | - I{rcpt_to} (list) is a list of the recipients addresses to pass to the SMTP host 282 | of the form C{[ 'a@b.com', c@d.com', ]}. This combine all recipients, 283 | I{carbone copy} addresses and I{blind carbone copy} addresses. 284 | - I{msg_id} (None or str) None if message_id_string==None else the generated value for 285 | the message-id. If not None, this I{Message-ID} is already written 286 | into the payload. 287 | 288 | >>> import email.mime.text 289 | >>> msg=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 290 | >>> # I could use build_mail() instead 291 | >>> payload, mail_from, rcpt_to, msg_id=complete_mail(msg, ('Me', 'me@foo.com'), 292 | ... [ ('Him', 'him@bar.com'), ], 'Non unicode subject', 'iso-8859-1', 293 | ... cc=['her@bar.com',], date=1313558269, headers=[('User-Agent', u'pyzmail'), ]) 294 | >>> print payload 295 | ... # 3.X encode User-Agent: using 'iso-8859-1' even if it contains only us-asccii 296 | ... # doctest: +ELLIPSIS 297 | Content-Type: text/plain; charset="us-ascii" 298 | MIME-Version: 1.0 299 | Content-Transfer-Encoding: 7bit 300 | From: Me 301 | To: Him 302 | Cc: her@bar.com 303 | Subject: =?iso-8859-1?q?Non_unicode_subject?= 304 | Date: ... 305 | User-Agent: ...pyzmail... 306 | 307 | The text. 308 | >>> print 'mail_from=%r rcpt_to=%r' % (mail_from, rcpt_to) 309 | mail_from='me@foo.com' rcpt_to=['him@bar.com', 'her@bar.com'] 310 | """ 311 | def getaddr(address): 312 | if isinstance(address, tuple): 313 | return address[1] 314 | else: 315 | return address 316 | 317 | mail_from=getaddr(sender[1]) 318 | rcpt_to=map(getaddr, recipients) 319 | rcpt_to.extend(map(getaddr, cc)) 320 | rcpt_to.extend(map(getaddr, bcc)) 321 | 322 | message['From'] = format_addresses([ sender, ], header_name='from', charset=default_charset) 323 | if recipients: 324 | message['To'] = format_addresses(recipients, header_name='to', charset=default_charset) 325 | if cc: 326 | message['Cc'] = format_addresses(cc, header_name='cc', charset=default_charset) 327 | message['Subject'] = email.header.Header(subject, default_charset) 328 | if date: 329 | utc_from_epoch=date 330 | else: 331 | utc_from_epoch=time.time() 332 | message['Date'] = email.utils.formatdate(utc_from_epoch, localtime=True) 333 | 334 | if message_id_string: 335 | msg_id=message['Message-Id']=email.utils.make_msgid(message_id_string) 336 | else: 337 | msg_id=None 338 | 339 | for field, value in headers: 340 | if isinstance(value, email.header.Header): 341 | message[field]=value 342 | else: 343 | message[field]=email.header.Header(value, default_charset) 344 | 345 | payload=message.as_string() 346 | 347 | return payload, mail_from, rcpt_to, msg_id 348 | 349 | def compose_mail(sender, recipients, subject, default_charset, text, html=None, attachments=[], embeddeds=[], cc=[], bcc=[], message_id_string=None, date=None, headers=[]): 350 | """ 351 | Compose an email regarding the arguments. Call L{build_mail()} and 352 | L{complete_mail()} at once. 353 | 354 | Read the B{parameters} descriptions of both functions L{build_mail()} and L{complete_mail()}. 355 | 356 | Returned value is the same as for L{build_mail()} and L{complete_mail()}. 357 | You can pass the returned values to L{send_mail()} or L{send_mail2()}. 358 | 359 | @rtype: tuple 360 | @return: B{(payload, mail_from, rcpt_to, msg_id)} 361 | 362 | >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'), [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 363 | """ 364 | message=build_mail(text, html, attachments, embeddeds) 365 | return complete_mail(message, sender, recipients, subject, default_charset, cc, bcc, message_id_string, date, headers) 366 | 367 | 368 | def send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None): 369 | """ 370 | Send the message to a SMTP host. Look at the L{send_mail()} documentation. 371 | L{send_mail()} call this function and catch all exceptions to convert them 372 | into a user friendly error message. The returned value 373 | is always a dictionary. It can be empty if all recipients have been 374 | accepted. 375 | 376 | @rtype: dict 377 | @return: This function return the value returnd by C{smtplib.SMTP.sendmail()} 378 | or raise the same exceptions. 379 | 380 | This method will return normally if the mail is accepted for at least one 381 | recipient. Otherwise it will raise an exception. That is, if this 382 | method does not raise an exception, then someone should get your mail. 383 | If this method does not raise an exception, it returns a dictionary, 384 | with one entry for each recipient that was refused. Each entry contains a 385 | tuple of the SMTP error code and the accompanying error message sent by the server. 386 | 387 | @raise smtplib.SMTPException: Look at the standard C{smtplib.SMTP.sendmail()} documentation. 388 | 389 | """ 390 | if smtp_mode=='ssl': 391 | smtp=smtplib.SMTP_SSL(smtp_host, smtp_port) 392 | else: 393 | smtp=smtplib.SMTP(smtp_host, smtp_port) 394 | if smtp_mode=='tls': 395 | smtp.starttls() 396 | 397 | if smtp_login and smtp_password: 398 | if sys.version_info<(3, 0): 399 | # python 2.x 400 | # login and password must be encoded 401 | # because HMAC used in CRAM_MD5 require non unicode string 402 | smtp.login(smtp_login.encode('utf-8'), smtp_password.encode('utf-8')) 403 | else: 404 | #python 3.x 405 | smtp.login(smtp_login, smtp_password) 406 | try: 407 | ret=smtp.sendmail(mail_from, rcpt_to, payload) 408 | finally: 409 | try: 410 | smtp.quit() 411 | except Exception, e: 412 | pass 413 | 414 | return ret 415 | 416 | def send_mail(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None): 417 | """ 418 | Send the message to a SMTP host. Handle SSL, TLS and authentication. 419 | I{payload}, I{mail_from} and I{rcpt_to} can come from values returned by 420 | L{complete_mail()}. This function call L{send_mail2()} but catch all 421 | exceptions and return friendly error message instead. 422 | 423 | @type payload: str 424 | @param payload: the mail content. 425 | @type mail_from: str 426 | @param mail_from: the sender address, for example: C{'me@domain.com'}. 427 | @type rcpt_to: list 428 | @param rcpt_to: The list of the recipient addresses in the form 429 | C{[ 'a@b.com', c@d.com', ]}. No names here, only email addresses. 430 | @type smtp_host: str 431 | @param smtp_host: the IP address or the name of the SMTP host. 432 | @type smtp_port: int 433 | @keyword smtp_port: the port to connect to on the SMTP host. Default is C{25}. 434 | @type smtp_mode: str 435 | @keyword smtp_mode: the way to connect to the SMTP host, can be: 436 | C{'normal'}, C{'ssl'} or C{'tls'}. default is C{'normal'} 437 | @type smtp_login: str or None 438 | @keyword smtp_login: If authentication is required, this is the login. 439 | Be carefull to I{UTF8} encode your login if it contains 440 | non I{us-ascii} characters. 441 | @type smtp_password: str or None 442 | @keyword smtp_password: If authentication is required, this is the password. 443 | Be carefull to I{UTF8} encode your password if it 444 | contains non I{us-ascii} characters. 445 | 446 | @rtype: dict or str 447 | @return: This function return a dictionary of failed recipients 448 | or a string with an error message. 449 | 450 | If all recipients have been accepted the dictionary is empty. If the 451 | returned value is a string, none of the recipients will get the message. 452 | 453 | The dictionary is exactly of the same sort as 454 | smtplib.SMTP.sendmail() returns with one entry for each recipient that 455 | was refused. Each entry contains a tuple of the SMTP error code and 456 | the accompanying error message sent by the server. 457 | 458 | Example: 459 | 460 | >>> send_mail('Subject: hello\\n\\nmessage', 'a@foo.com', [ 'b@bar.com', ], 'localhost') #doctest: +SKIP 461 | {} 462 | 463 | Here is how to use the returned value:: 464 | if isinstance(ret, dict): 465 | if ret: 466 | print 'failed' recipients: 467 | for recipient, (code, msg) in ret.iteritems(): 468 | print 'code=%d recipient=%s\terror=%s' % (code, recipient, msg) 469 | else: 470 | print 'success' 471 | else: 472 | print 'Error:', ret 473 | 474 | To use your GMail account to send your mail:: 475 | smtp_host='smtp.gmail.com' 476 | smtp_port=587 477 | smtp_mode='tls' 478 | smtp_login='your.gmail.addresse@gmail.com' 479 | smtp_password='your.gmail.password' 480 | 481 | Use your GMail address for the sender ! 482 | 483 | """ 484 | 485 | error=dict() 486 | try: 487 | ret=send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port, smtp_mode, smtp_login, smtp_password) 488 | except (socket.error, ), e: 489 | error='server %s:%s not responding: %s' % (smtp_host, smtp_port, e) 490 | except smtplib.SMTPAuthenticationError, e: 491 | error='authentication error: %s' % (e, ) 492 | except smtplib.SMTPRecipientsRefused, e: 493 | # code, error=e.recipients[recipient_addr] 494 | error='all recipients refused: '+', '.join(e.recipients.keys()) 495 | except smtplib.SMTPSenderRefused, e: 496 | # e.sender, e.smtp_code, e.smtp_error 497 | error='sender refused: %s' % (e.sender, ) 498 | except smtplib.SMTPDataError, e: 499 | error='SMTP protocol mismatch: %s' % (e, ) 500 | except smtplib.SMTPHeloError, e: 501 | error="server didn't reply properly to the HELO greeting: %s" % (e, ) 502 | except smtplib.SMTPException, e: 503 | error='SMTP error: %s' % (e, ) 504 | # except Exception, e: 505 | # raise # unknown error 506 | else: 507 | # failed addresses and error messages 508 | error=ret 509 | 510 | return error 511 | 512 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/parse.py: -------------------------------------------------------------------------------- 1 | # 2 | # pyzmail/parse.py 3 | # (c) Alain Spineux 4 | # http://www.magiksys.net/pyzmail 5 | # Released under LGPL 6 | 7 | """ 8 | Useful functions to parse emails 9 | 10 | @var email_address_re: a regex that match well formed email address (from perlfaq9) 11 | @undocumented: atom_rfc2822 12 | @undocumented: atom_posfix_restricted 13 | @undocumented: atom 14 | @undocumented: dot_atom 15 | @undocumented: local 16 | @undocumented: domain_lit 17 | @undocumented: domain 18 | @undocumented: addr_spec 19 | """ 20 | 21 | import re 22 | import StringIO 23 | import email 24 | import email.errors 25 | import email.header 26 | import email.message 27 | import mimetypes 28 | 29 | from utils import * 30 | 31 | # email address REGEX matching the RFC 2822 spec from perlfaq9 32 | # my $atom = qr{[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+}; 33 | # my $dot_atom = qr{$atom(?:\.$atom)*}; 34 | # my $quoted = qr{"(?:\\[^\r\n]|[^\\"])*"}; 35 | # my $local = qr{(?:$dot_atom|$quoted)}; 36 | # my $domain_lit = qr{\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\]}; 37 | # my $domain = qr{(?:$dot_atom|$domain_lit)}; 38 | # my $addr_spec = qr{$local\@$domain}; 39 | # 40 | # Python's translation 41 | atom_rfc2822=r"[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+" 42 | atom_posfix_restricted=r"[a-zA-Z0-9_#\$&'*+/=?\^`{}~|\-]+" # without '!' and '%' 43 | atom=atom_rfc2822 44 | dot_atom=atom + r"(?:\." + atom + ")*" 45 | quoted=r'"(?:\\[^\r\n]|[^\\"])*"' 46 | local="(?:" + dot_atom + "|" + quoted + ")" 47 | domain_lit=r"\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\]" 48 | domain="(?:" + dot_atom + "|" + domain_lit + ")" 49 | addr_spec=local + "\@" + domain 50 | # and the result 51 | email_address_re=re.compile('^'+addr_spec+'$') 52 | 53 | class MailPart: 54 | """ 55 | Data related to a mail part (aka message content, attachment or 56 | embedded content in an email) 57 | 58 | @type charset: str or None 59 | @ivar charset: the encoding of the I{get_payload()} content if I{type} is 'text/*' 60 | and charset has been specified in the message 61 | @type content_id: str or None 62 | @ivar content_id: the MIME Content-ID if specified in the message. 63 | @type description: str or None 64 | @ivar description: the MIME Content-Description if specified in the message. 65 | @type disposition: str or None 66 | @ivar disposition: C{None}, C{'inline'} or C{'attachment'} depending 67 | the MIME Content-Disposition value 68 | @type filename: unicode or None 69 | @ivar filename: the name of the file, if specified in the message. 70 | @type part: inherit from email.mime.base.MIMEBase 71 | @ivar part: the related part inside the message. 72 | @type is_body: str or None 73 | @ivar is_body: None if this part is not the mail content itself (an 74 | attachment or embedded content), C{'text/plain'} if this part is the 75 | text content or C{'text/html'} if this part is the HTML version. 76 | @type sanitized_filename: str or None 77 | @ivar sanitized_filename: This field is filled by L{PyzMessage} to store 78 | a valid unique filename related or not with the original filename. 79 | @type type: str 80 | @ivar type: the MIME type, like 'text/plain', 'image/png', 'application/msword' ... 81 | """ 82 | 83 | def __init__(self, part, filename=None, type=None, charset=None, content_id=None, description=None, disposition=None, sanitized_filename=None, is_body=None): 84 | """ 85 | Create an mail part and initialize all attributes 86 | """ 87 | self.part=part # original python part 88 | self.filename=filename # filename in unicode (if any) 89 | self.type=type # the mime-type 90 | self.charset=charset # the charset (if any) 91 | self.description=description # if any 92 | self.disposition=disposition # 'inline', 'attachment' or None 93 | self.sanitized_filename=sanitized_filename # cleanup your filename here (TODO) 94 | self.is_body=is_body # usually in (None, 'text/plain' or 'text/html') 95 | self.content_id=content_id # if any 96 | if self.content_id: 97 | # strip '<>' to ease search and replace in "root" content (TODO) 98 | if self.content_id.startswith('<') and self.content_id.endswith('>'): 99 | self.content_id=self.content_id[1:-1] 100 | 101 | def get_payload(self): 102 | """ 103 | decode and return part payload. if I{type} is 'text/*' and I{charset} 104 | not C{None}, be careful to take care of the text encoding. Use 105 | something like C{part.get_payload().decode(part.charset)} 106 | """ 107 | 108 | payload=None 109 | if self.type.startswith('message/'): 110 | # I don't use msg.as_string() because I want to use mangle_from_=False 111 | if sys.version_info<(3, 0): 112 | # python 2.x 113 | from email.generator import Generator 114 | fp = StringIO.StringIO() 115 | g = Generator(fp, mangle_from_=False) 116 | g.flatten(self.part, unixfrom=False) 117 | payload=fp.getvalue() 118 | else: 119 | # support only for python >= 3.2 120 | from email.generator import BytesGenerator 121 | import io 122 | fp = io.BytesIO() 123 | g = BytesGenerator(fp, mangle_from_=False) 124 | g.flatten(self.part, unixfrom=False) 125 | payload=fp.getvalue() 126 | 127 | else: 128 | payload=self.part.get_payload(decode=True) 129 | return payload 130 | 131 | def __repr__(self): 132 | st=u'MailPart<' 133 | if self.is_body: 134 | st+=u'*' 135 | st+=self.type 136 | if self.charset: 137 | st+=' charset='+self.charset 138 | if self.filename: 139 | st+=' filename='+self.filename 140 | if self.content_id: 141 | st+=' content_id='+self.content_id 142 | st+=' len=%d' % (len(self.get_payload()), ) 143 | st+=u'>' 144 | return st 145 | 146 | 147 | 148 | _line_end_re=re.compile('\r\n|\n\r|\n|\r') 149 | 150 | def _friendly_header(header): 151 | """ 152 | Convert header returned by C{email.message.Message.get()} into a 153 | user friendly string. 154 | 155 | Py3k C{email.message.Message.get()} return C{header.Header()} with charset 156 | set to C{charset.UNKNOWN8BIT} when the header contains invalid characters, 157 | else it return I{str} as Python 2.X does 158 | 159 | @type header: str or email.header.Header 160 | @param header: the header to convert into a user friendly string 161 | 162 | @rtype: str 163 | @returns: the converter header 164 | """ 165 | 166 | save=header 167 | if isinstance(header, email.header.Header): 168 | header=str(header) 169 | 170 | return re.sub(_line_end_re, ' ', header) 171 | 172 | def decode_mail_header(value, default_charset='us-ascii'): 173 | """ 174 | Decode a header value into a unicode string. 175 | Works like a more smarter python 176 | C{u"".join(email.header.decode_header()} function 177 | 178 | @type value: str 179 | @param value: the value of the header. 180 | @type default_charset: str 181 | @keyword default_charset: if one charset used in the header (multiple charset 182 | can be mixed) is unknown, then use this charset instead. 183 | 184 | >>> decode_mail_header('=?iso-8859-1?q?Courrier_=E8lectronique_en_Fran=E7ais?=') 185 | u'Courrier \\xe8lectronique en Fran\\xe7ais' 186 | """ 187 | 188 | # value=_friendly_header(value) 189 | try: 190 | headers=email.header.decode_header(value) 191 | except email.errors.HeaderParseError: 192 | # this can append in email.base64mime.decode(), for example for this value: 193 | # '=?UTF-8?B?15HXmdeh15jXqNeVINeY15DXpteUINeTJ9eV16jXlSDXkdeg15XXldeUINem15PXpywg15TXptei16bXldei15nXnSDXqdecINek15zXmdeZ?==?UTF-8?B?157XldeR15nXnCwg157Xldek16Ig157Xl9eV15wg15HXodeV15bXnyDXk9ec15DXnCDXldeh15gg157Xl9eR16rXldeqINep15wg15HXmdeQ?==?UTF-8?B?15zXmNeZ?=' 194 | # then return a sanitized ascii string 195 | # TODO: some improvements are possible here, but a failure here is 196 | # unlikely 197 | return value.encode('us-ascii', 'replace').decode('us-ascii') 198 | else: 199 | for i, (text, charset) in enumerate(headers): 200 | # python 3.x 201 | # email.header.decode_header('a') -> [('a', None)] 202 | # email.header.decode_header('a =?ISO-8859-1?Q?foo?= b') 203 | # --> [(b'a', None), (b'foo', 'iso-8859-1'), (b'b', None)] 204 | # in Py3 text is sometime str and sometime byte :-( 205 | # python 2.x 206 | # email.header.decode_header('a') -> [('a', None)] 207 | # email.header.decode_header('a =?ISO-8859-1?Q?foo?= b') 208 | # --> [('a', None), ('foo', 'iso-8859-1'), ('b', None)] 209 | if (charset is None and sys.version_info>=(3, 0)): 210 | # Py3 211 | if isinstance(text, str): 212 | # convert Py3 string into bytes string to be sure their is no 213 | # non us-ascii chars and because next line expect byte string 214 | text=text.encode('us-ascii', 'replace') 215 | try: 216 | headers[i]=text.decode(charset or 'us-ascii', 'replace') 217 | except LookupError: 218 | # if the charset is unknown, force default 219 | headers[i]=text.decode(default_charset, 'replace') 220 | 221 | return u"".join(headers) 222 | 223 | def get_mail_addresses(message, header_name): 224 | """ 225 | retrieve all email addresses from one message header 226 | 227 | @type message: email.message.Message 228 | @param message: the email message 229 | @type header_name: str 230 | @param header_name: the name of the header, can be 'from', 'to', 'cc' or 231 | any other header containing one or more email addresses 232 | @rtype: list 233 | @returns: a list of the addresses in the form of tuples 234 | C{[(u'Name', 'addresse@domain.com'), ...]} 235 | 236 | >>> import email 237 | >>> import email.mime.text 238 | >>> msg=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 239 | >>> msg['From']=email.email.utils.formataddr(('Me', 'me@foo.com')) 240 | >>> msg['To']=email.email.utils.formataddr(('A', 'a@foo.com'))+', '+email.email.utils.formataddr(('B', 'b@foo.com')) 241 | >>> print msg.as_string(unixfrom=False) 242 | Content-Type: text/plain; charset="us-ascii" 243 | MIME-Version: 1.0 244 | Content-Transfer-Encoding: 7bit 245 | From: Me 246 | To: A , B 247 | 248 | The text. 249 | >>> get_mail_addresses(msg, 'from') 250 | [(u'Me', 'me@foo.com')] 251 | >>> get_mail_addresses(msg, 'to') 252 | [(u'A', 'a@foo.com'), (u'B', 'b@foo.com')] 253 | """ 254 | addrs=email.utils.getaddresses([ _friendly_header(h) for h in message.get_all(header_name, [])]) 255 | for i, (addr_name, addr) in enumerate(addrs): 256 | if not addr_name and addr: 257 | # only one string! Is it the address or the address name ? 258 | # use the same for both and see later 259 | addr_name=addr 260 | 261 | if is_usascii(addr): 262 | # address must be ascii only and must match address regex 263 | if not email_address_re.match(addr): 264 | addr='' 265 | else: 266 | addr='' 267 | addrs[i]=(decode_mail_header(addr_name), addr) 268 | return addrs 269 | 270 | def get_filename(part): 271 | """ 272 | Find the filename of a mail part. Many MUA send attachments with the 273 | filename in the I{name} parameter of the I{Content-type} header instead 274 | of in the I{filename} parameter of the I{Content-Disposition} header. 275 | 276 | @type part: inherit from email.mime.base.MIMEBase 277 | @param part: the mail part 278 | @rtype: None or unicode 279 | @returns: the filename or None if not found 280 | 281 | >>> import email.mime.image 282 | >>> attach=email.mime.image.MIMEImage('data', 'png') 283 | >>> attach.add_header('Content-Disposition', 'attachment', filename='image.png') 284 | >>> get_filename(attach) 285 | u'image.png' 286 | >>> print attach.as_string(unixfrom=False) 287 | Content-Type: image/png 288 | MIME-Version: 1.0 289 | Content-Transfer-Encoding: base64 290 | Content-Disposition: attachment; filename="image.png" 291 | 292 | ZGF0YQ== 293 | >>> import email.mime.text 294 | >>> attach=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 295 | >>> attach.add_header('Content-Disposition', 'attachment', filename=('iso-8859-1', 'fr', u'Fran\\xe7ais.txt'.encode('iso-8859-1'))) 296 | >>> get_filename(attach) 297 | u'Fran\\xe7ais.txt' 298 | >>> print attach.as_string(unixfrom=False) 299 | Content-Type: text/plain; charset="us-ascii" 300 | MIME-Version: 1.0 301 | Content-Transfer-Encoding: 7bit 302 | Content-Disposition: attachment; filename*="iso-8859-1'fr'Fran%E7ais.txt" 303 | 304 | The text. 305 | """ 306 | filename=part.get_param('filename', None, 'content-disposition') 307 | if not filename: 308 | filename=part.get_param('name', None) # default is 'content-type' 309 | 310 | if filename: 311 | if isinstance(filename, tuple): 312 | # RFC 2231 must be used to encode parameters inside MIME header 313 | filename=email.utils.collapse_rfc2231_value(filename).strip() 314 | else: 315 | # But a lot of MUA erroneously use RFC 2047 instead of RFC 2231 316 | # in fact anybody missuse RFC2047 here !!! 317 | filename=decode_mail_header(filename) 318 | 319 | return filename 320 | 321 | def _search_message_content(contents, part): 322 | """ 323 | recursive search of message content (text or HTML) inside 324 | the structure of the email. Used by L{search_message_content()} 325 | 326 | @type contents: dict 327 | @param contents: contents already found in parents or brothers I{parts}. 328 | The dictionary will be completed as and when. key is the MIME type of the part. 329 | @type part: inherit email.mime.base.MIMEBase 330 | @param part: the part of the mail to look inside recursively. 331 | """ 332 | type=part.get_content_type() 333 | if part.is_multipart(): # type.startswith('multipart/'): 334 | # explore only True 'multipart/*' 335 | # because 'messages/rfc822' are 'multipart/*' too but 336 | # must not be explored here 337 | if type=='multipart/related': 338 | # the first part or the one pointed by start 339 | start=part.get_param('start', None) 340 | related_type=part.get_param('type', None) 341 | for i, subpart in enumerate(part.get_payload()): 342 | if (not start and i==0) or (start and start==subpart.get('Content-Id')): 343 | _search_message_content(contents, subpart) 344 | return 345 | elif type=='multipart/alternative': 346 | # all parts are candidates and latest is the best 347 | for subpart in part.get_payload(): 348 | _search_message_content(contents, subpart) 349 | elif type in ('multipart/report', 'multipart/signed'): 350 | # only the first part is candidate 351 | try: 352 | subpart=part.get_payload()[0] 353 | except IndexError: 354 | return 355 | else: 356 | _search_message_content(contents, subpart) 357 | return 358 | 359 | elif type=='multipart/encrypted': 360 | # the second part is the good one, but we need to de-crypt it 361 | # using the first part. Do nothing 362 | return 363 | 364 | else: 365 | # unknown types must be handled as 'multipart/mixed' 366 | # This is the peace of code that could probably be improved, 367 | # I use a heuristic : if not already found, use first valid non 368 | # 'attachment' parts found 369 | for subpart in part.get_payload(): 370 | tmp_contents=dict() 371 | _search_message_content(tmp_contents, subpart) 372 | for k, v in tmp_contents.iteritems(): 373 | if not subpart.get_param('attachment', None, 'content-disposition')=='': 374 | # if not an attachment, initiate value if not already found 375 | contents.setdefault(k, v) 376 | return 377 | else: 378 | contents[part.get_content_type().lower()]=part 379 | return 380 | 381 | return 382 | 383 | def search_message_content(mail): 384 | """ 385 | search of message content (text or HTML) inside 386 | the structure of the mail. This function is used by L{get_mail_parts()} 387 | to set the C{is_body} part of the L{MailPart}s 388 | 389 | @type mail: inherit from email.message.Message 390 | @param mail: the message to search in. 391 | @rtype: dict 392 | @returns: a dictionary of the form C{{'text/plain': text_part, 'text/html': html_part}} 393 | where text_part and html_part inherite from C{email.mime.text.MIMEText} 394 | and are respectively the I{text} and I{HTML} version of the message content. 395 | One part can be missing. The dictionay can aven be empty if none of the 396 | parts math the requirements to be considered as the content. 397 | """ 398 | contents=dict() 399 | _search_message_content(contents, mail) 400 | return contents 401 | 402 | def get_mail_parts(msg): 403 | """ 404 | return a list of all parts of the message as a list of L{MailPart}. 405 | Retrieve parts attributes to fill in L{MailPart} object. 406 | 407 | @type msg: inherit email.message.Message 408 | @param msg: the message 409 | @rtype: list 410 | @returns: list of mail parts 411 | 412 | >>> import email.mime.multipart 413 | >>> msg=email.mime.multipart.MIMEMultipart(boundary='===limit1==') 414 | >>> import email.mime.text 415 | >>> txt=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 416 | >>> msg.attach(txt) 417 | >>> import email.mime.image 418 | >>> image=email.mime.image.MIMEImage('data', 'png') 419 | >>> image.add_header('Content-Disposition', 'attachment', filename='image.png') 420 | >>> msg.attach(image) 421 | >>> print msg.as_string(unixfrom=False) 422 | Content-Type: multipart/mixed; boundary="===limit1==" 423 | MIME-Version: 1.0 424 | 425 | --===limit1== 426 | Content-Type: text/plain; charset="us-ascii" 427 | MIME-Version: 1.0 428 | Content-Transfer-Encoding: 7bit 429 | 430 | The text. 431 | --===limit1== 432 | Content-Type: image/png 433 | MIME-Version: 1.0 434 | Content-Transfer-Encoding: base64 435 | Content-Disposition: attachment; filename="image.png" 436 | 437 | ZGF0YQ== 438 | --===limit1==-- 439 | 440 | >>> parts=get_mail_parts(msg) 441 | >>> parts 442 | [MailPart<*text/plain charset=us-ascii len=9>, MailPart] 443 | >>> # the star "*" means this is the mail content, not an attachment 444 | >>> parts[0].get_payload().decode(parts[0].charset) 445 | u'The text.' 446 | >>> parts[1].filename, len(parts[1].get_payload()) 447 | (u'image.png', 4) 448 | 449 | """ 450 | mailparts=[] 451 | 452 | # retrieve messages of the email 453 | contents=search_message_content(msg) 454 | # reverse contents dict 455 | parts=dict((v,k) for k, v in contents.iteritems()) 456 | 457 | # organize the stack to handle deep first search 458 | stack=[ msg, ] 459 | while stack: 460 | part=stack.pop(0) 461 | type=part.get_content_type() 462 | if type.startswith('message/'): 463 | # ('message/delivery-status', 'message/rfc822', 'message/disposition-notification'): 464 | # I don't want to explore the tree deeper her and just save source using msg.as_string() 465 | # but I don't use msg.as_string() because I want to use mangle_from_=False 466 | filename=get_filename(part) 467 | filename=filename if filename else 'message.eml' 468 | mailparts.append(MailPart(part, filename=filename, type=type, charset=part.get_param('charset'), description=part.get('Content-Description'))) 469 | elif part.is_multipart(): 470 | # insert new parts at the beginning of the stack (deep first search) 471 | stack[:0]=part.get_payload() 472 | else: 473 | charset=part.get_param('charset') 474 | filename=get_filename(part) 475 | 476 | disposition=None 477 | if part.get_param('inline', None, 'content-disposition')=='': 478 | disposition='inline' 479 | elif part.get_param('attachment', None, 'content-disposition')=='': 480 | disposition='attachment' 481 | 482 | mailparts.append(MailPart(part, filename=filename, type=type, charset=charset, content_id=part.get('Content-Id'), description=part.get('Content-Description'), disposition=disposition, is_body=parts.get(part, False))) 483 | 484 | return mailparts 485 | 486 | 487 | def decode_text(payload, charset, default_charset): 488 | """ 489 | Try to decode text content by trying multiple charset until success. 490 | First try I{charset}, else try I{default_charset} finally 491 | try popular charsets in order : ascii, utf-8, utf-16, windows-1252, cp850 492 | If all fail then use I{default_charset} and replace wrong characters 493 | 494 | @type payload: str 495 | @param payload: the content to decode 496 | @type charset: str or None 497 | @param charset: the first charset to try if != C{None} 498 | @type default_charset: str or None 499 | @param default_charset: the second charset to try if != C{None} 500 | 501 | @rtype: tuple 502 | @returns: a tuple of the form C{(payload, charset)} 503 | - I{payload}: this is the decoded payload if charset is not None and 504 | payload is a unicode string 505 | - I{charset}: the charset that was used to decode I{payload} If charset is 506 | C{None} then something goes wrong: if I{payload} is unicode then 507 | invalid characters have been replaced and the used charset is I{default_charset} 508 | else, if I{payload} is still byte string then nothing has been done. 509 | 510 | 511 | """ 512 | for chset in [ charset, default_charset, 'ascii', 'utf-8', 'utf-16', 'windows-1252', 'cp850' ]: 513 | if chset: 514 | try: 515 | return payload.decode(chset), chset 516 | except UnicodeError: 517 | pass 518 | 519 | if default_charset: 520 | return payload.decode(chset, 'replace'), None 521 | 522 | return payload, None 523 | 524 | class PyzMessage(email.message.Message): 525 | """ 526 | Inherit from email.message.Message. Combine L{get_mail_parts()}, 527 | L{get_mail_addresses()} and L{decode_mail_header()} into a 528 | B{convenient} object to access mail contents and attributes. 529 | This class also B{sanitize} part filenames. 530 | 531 | @type mailparts: list of L{MailPart} 532 | @ivar mailparts: list of L{MailPart} objects composing the email, I{text_part} 533 | and I{html_part} are part of this list as are other attachements and embedded 534 | contents. 535 | @type text_part: L{MailPart} or None 536 | @ivar text_part: the L{MailPart} object that contains the I{text} 537 | version of the message, None if the mail has not I{text} content. 538 | @type html_part: L{MailPart} or None 539 | @ivar html_part: the L{MailPart} object that contains the I{HTML} 540 | version of the message, None if the mail has not I{HTML} content. 541 | 542 | @note: Sample: 543 | 544 | >>> raw='''Content-Type: text/plain; charset="us-ascii" 545 | ... MIME-Version: 1.0 546 | ... Content-Transfer-Encoding: 7bit 547 | ... Subject: The subject 548 | ... From: Me 549 | ... To: A , B 550 | ... 551 | ... The text. 552 | ... ''' 553 | >>> msg=PyzMessage.factory(raw) 554 | >>> print 'Subject: %r' % (msg.get_subject(), ) 555 | Subject: u'The subject' 556 | >>> print 'From: %r' % (msg.get_address('from'), ) 557 | From: (u'Me', 'me@foo.com') 558 | >>> print 'To: %r' % (msg.get_addresses('to'), ) 559 | To: [(u'A', 'a@foo.com'), (u'B', 'b@foo.com')] 560 | >>> print 'Cc: %r' % (msg.get_addresses('cc'), ) 561 | Cc: [] 562 | >>> for mailpart in msg.mailparts: 563 | ... print ' %sfilename=%r sanitized_filename=%r type=%s charset=%s desc=%s size=%d' % ('*'if mailpart.is_body else ' ', mailpart.filename, mailpart.sanitized_filename, mailpart.type, mailpart.charset, mailpart.part.get('Content-Description'), 0 if mailpart.get_payload()==None else len(mailpart.get_payload())) 564 | ... if mailpart.is_body=='text/plain': 565 | ... payload, used_charset=decode_text(mailpart.get_payload(), mailpart.charset, None) 566 | ... print ' >', payload.split('\\n')[0] 567 | ... 568 | *filename=None sanitized_filename='text.txt' type=text/plain charset=us-ascii desc=None size=10 569 | > The text. 570 | """ 571 | 572 | @staticmethod 573 | def smart_parser(input): 574 | """ 575 | Use the appropriate parser and return a email.message.Message object 576 | (this is not a L{PyzMessage} object) 577 | 578 | @type input: string, file, bytes, binary_file or email.message.Message 579 | @param input: the source of the message 580 | @rtype: email.message.Message 581 | @returns: the message 582 | """ 583 | if isinstance(input, email.message.Message): 584 | return input 585 | 586 | if sys.version_info<(3, 0): 587 | # python 2.x 588 | if isinstance(input, basestring): 589 | return email.message_from_string(input) 590 | elif hasattr(input, 'read') and hasattr(input, 'readline'): 591 | return email.message_from_file(input) 592 | else: 593 | raise ValueError, 'input must be a string, a file or a Message' 594 | else: 595 | # python 3.x 596 | if isinstance(input, str): 597 | return email.message_from_string(input) 598 | elif isinstance(input, bytes): 599 | # python >= 3.2 only 600 | return email.message_from_bytes(input) 601 | elif hasattr(input, 'read') and hasattr(input, 'readline'): 602 | if hasattr(input, 'encoding'): 603 | # python >= 3.2 only 604 | return email.message_from_file(input) 605 | else: 606 | return email.message_from_binary_file(input) 607 | else: 608 | raise ValueError, 'input must be a string a bytes, a file or a Message' 609 | 610 | @staticmethod 611 | def factory(input): 612 | """ 613 | Use the appropriate parser and return a L{PyzMessage} object 614 | see L{smart_parser} 615 | @type input: string, file, bytes, binary_file or email.message.Message 616 | @param input: the source of the message 617 | @rtype: L{PyzMessage} 618 | @returns: the L{PyzMessage} message 619 | """ 620 | return PyzMessage(PyzMessage.smart_parser(input)) 621 | 622 | 623 | def __init__(self, message): 624 | """ 625 | Initialize the object with data coming from I{message}. 626 | 627 | @type message: inherit email.message.Message 628 | @param message: The message 629 | """ 630 | if not isinstance(message, email.message.Message): 631 | raise ValueError, "message must inherit from email.message.Message use PyzMessage.factory() instead" 632 | self.__dict__.update(message.__dict__) 633 | 634 | self.mailparts=get_mail_parts(self) 635 | self.text_part=None 636 | self.html_part=None 637 | 638 | filenames=[] 639 | for part in self.mailparts: 640 | ext=mimetypes.guess_extension(part.type) 641 | if not ext: 642 | # default to .bin 643 | ext='.bin' 644 | elif ext=='.ksh': 645 | # guess_extension() is not very accurate, .txt is more 646 | # appropriate than .ksh 647 | ext='.txt' 648 | 649 | sanitized_filename=sanitize_filename(part.filename, part.type.split('/', 1)[0], ext) 650 | sanitized_filename=handle_filename_collision(sanitized_filename, filenames) 651 | filenames.append(sanitized_filename.lower()) 652 | part.sanitized_filename=sanitized_filename 653 | 654 | if part.is_body=='text/plain': 655 | self.text_part=part 656 | 657 | if part.is_body=='text/html': 658 | self.html_part=part 659 | 660 | def get_addresses(self, name): 661 | """ 662 | return the I{name} header value as an list of addresses tuple as 663 | returned by L{get_mail_addresses()} 664 | 665 | @type name: str 666 | @param name: the name of the header to read value from: 'to', 'cc' are 667 | valid I{name} here. 668 | @rtype: tuple 669 | @returns: a tuple of the form C{('Sender Name', 'sender.address@domain.com')} 670 | or C{('', '')} if no header match that I{name}. 671 | """ 672 | return get_mail_addresses(self, name) 673 | 674 | def get_address(self, name): 675 | """ 676 | return the I{name} header value as an address tuple as returned by 677 | L{get_mail_addresses()} 678 | 679 | @type name: str 680 | @param name: the name of the header to read value from: : C{'from'} can 681 | be used to return the sender address. 682 | @rtype: list of tuple 683 | @returns: a list of tuple of the form C{[('Recipient Name', 'recipient.address@domain.com'), ...]} 684 | or an empty list if no header match that I{name}. 685 | """ 686 | value=get_mail_addresses(self, name) 687 | if value: 688 | return value[0] 689 | else: 690 | return ('', '') 691 | 692 | def get_subject(self, default=''): 693 | """ 694 | return the RFC2047 decoded subject. 695 | 696 | @type default: any 697 | @param default: The value to return if the message has no I{Subject} 698 | @rtype: unicode 699 | @returns: the subject or C{default} 700 | """ 701 | return self.get_decoded_header('subject', default) 702 | 703 | def get_decoded_header(self, name, default=''): 704 | """ 705 | return decoded header I{name} using RFC2047. Always use this function 706 | to access header, because any header can contain invalid characters 707 | and this function sanitize the string and avoid unicode exception later 708 | in your program. 709 | EVEN for date, I already saw a "Center box bar horizontal" instead 710 | of a minus character. 711 | 712 | @type name: str 713 | @param name: the name of the header to read value from. 714 | @type default: any 715 | @param default: The value to return if the I{name} field don't exist 716 | in this message. 717 | @rtype: unicode 718 | @returns: the value of the header having that I{name} or C{default} if no 719 | header have that name. 720 | """ 721 | value=self.get(name) 722 | if value==None: 723 | value=default 724 | else: 725 | value=decode_mail_header(value) 726 | return value 727 | 728 | class PzMessage(PyzMessage): 729 | """ 730 | Old name and interface for PyzMessage. 731 | B{Deprecated} 732 | """ 733 | 734 | def __init__(self, input): 735 | """ 736 | Initialize the object with data coming from I{input}. 737 | 738 | @type input: str or file or email.message.Message 739 | @param input: used as the raw content for the email, can be a string, 740 | a file object or an email.message.Message object. 741 | """ 742 | PyzMessage.__init__(self, self.smart_parser(input)) 743 | 744 | 745 | def message_from_string(s, *args, **kws): 746 | """ 747 | Parse a string into a L{PyzMessage} object model. 748 | @type s: str 749 | @param s: the input string 750 | @rtype: L{PyzMessage} 751 | @return: the L{PyzMessage} object 752 | """ 753 | return PyzMessage(email.message_from_string(s, *args, **kws)) 754 | 755 | def message_from_file(fp, *args, **kws): 756 | """ 757 | Read a file and parse its contents into a L{PyzMessage} object model. 758 | @type fp: text_file 759 | @param fp: the input file (must be open in text mode if Python >= 3.0) 760 | @rtype: L{PyzMessage} 761 | @return: the L{PyzMessage} object 762 | """ 763 | return PyzMessage(email.message_from_file(fp, *args, **kws)) 764 | 765 | def message_from_bytes(s, *args, **kws): 766 | """ 767 | Parse a bytes string into a L{PyzMessage} object model. 768 | B{(Python >= 3.2)} 769 | @type s: bytes 770 | @param s: the input bytes string 771 | @rtype: L{PyzMessage} 772 | @return: the L{PyzMessage} object 773 | """ 774 | return PyzMessage(email.message_from_bytes(s, *args, **kws)) 775 | 776 | def message_from_binary_file(fp, *args, **kws): 777 | """ 778 | Read a binary file and parse its contents into a L{PyzMessage} object model. 779 | B{(Python >= 3.2)} 780 | @type fp: binary_file 781 | @param fp: the input file, must be open in binary mode 782 | @rtype: L{PyzMessage} 783 | @return: the L{PyzMessage} object 784 | """ 785 | return PyzMessage(email.message_from_binary_file(fp, *args, **kws)) 786 | 787 | 788 | if __name__ == "__main__": 789 | import sys 790 | 791 | if len(sys.argv)<=1: 792 | print 'usage : %s filename' % sys.argv[0] 793 | print 'read an email from file and display a resume of its content' 794 | sys.exit(1) 795 | 796 | msg=PyzMessage.factory(open(sys.argv[1], 'rb')) 797 | 798 | print 'Subject: %r' % (msg.get_subject(), ) 799 | print 'From: %r' % (msg.get_address('from'), ) 800 | print 'To: %r' % (msg.get_addresses('to'), ) 801 | print 'Cc: %r' % (msg.get_addresses('cc'), ) 802 | print 'Date: %r' % (msg.get_decoded_header('date', ''), ) 803 | print 'Message-Id: %r' % (msg.get_decoded_header('message-id', ''), ) 804 | 805 | for mailpart in msg.mailparts: 806 | # dont forget to be careful to sanitize 'filename' and be carefull 807 | # for filename collision, to before to save : 808 | print ' %sfilename=%r type=%s charset=%s desc=%s size=%d' % ('*'if mailpart.is_body else ' ', mailpart.filename, mailpart.type, mailpart.charset, mailpart.part.get('Content-Description'), 0 if mailpart.get_payload()==None else len(mailpart.get_payload())) 809 | 810 | if mailpart.is_body=='text/plain': 811 | # print first 3 lines 812 | payload, used_charset=decode_text(mailpart.get_payload(), mailpart.charset, None) 813 | for line in payload.split('\n')[:3]: 814 | # be careful console can be unable to display unicode characters 815 | if line: 816 | print ' >', line 817 | 818 | 819 | 820 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspineux/pyzmail/e1a19edc86dbae59b67e83dd8a0ef5ee5f663dbb/pyzmail/pyzmail/tests/__init__.py -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/test_both.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pyzmail 3 | from pyzmail.generate import * 4 | from pyzmail.parse import * 5 | 6 | class TestBoth(unittest.TestCase): 7 | 8 | def setUp(self): 9 | pass 10 | 11 | def test_compose_and_parse(self): 12 | """test generate and parse""" 13 | 14 | sender=(u'Me', 'me@foo.com') 15 | recipients=[(u'Him', 'him@bar.com'), 'just@me.com'] 16 | subject=u'Le sujet en Fran\xe7ais' 17 | text_content=u'Bonjour aux Fran\xe7ais' 18 | prefered_encoding='iso-8859-1' 19 | text_encoding='iso-8859-1' 20 | attachments=[('attached content', 'text', 'plain', 'textfile1.txt', 'us-ascii'), 21 | (u'Fran\xe7ais', 'text', 'plain', 'textfile2.txt', 'iso-8859-1'), 22 | ('Fran\xe7ais', 'text', 'plain', 'textfile3.txt', 'iso-8859-1'), 23 | (b'image', 'image', 'jpg', 'imagefile.jpg', None), 24 | ] 25 | embeddeds=[(u'embedded content', 'text', 'plain', 'embedded', 'us-ascii'), 26 | (b'picture', 'image', 'png', 'picture', None), 27 | ] 28 | headers=[ ('X-extra', u'extra value'), ('X-extra2', u"Seconde ent\xe8te"), ('X-extra3', u'last extra'),] 29 | 30 | message_id_string='pyzmail' 31 | date=1313558269 32 | 33 | payload, mail_from, rcpt_to, msg_id=pyzmail.compose_mail(\ 34 | sender, \ 35 | recipients, \ 36 | subject, \ 37 | prefered_encoding, \ 38 | (text_content, text_encoding), \ 39 | html=None, \ 40 | attachments=attachments, \ 41 | embeddeds=embeddeds, \ 42 | headers=headers, \ 43 | message_id_string=message_id_string, \ 44 | date=date\ 45 | ) 46 | 47 | msg=PyzMessage.factory(payload) 48 | 49 | self.assertEqual(sender, msg.get_address('from')) 50 | self.assertEqual(recipients[0], msg.get_addresses('to')[0]) 51 | self.assertEqual(recipients[1], msg.get_addresses('to')[1][1]) 52 | self.assertEqual(subject, msg.get_subject()) 53 | self.assertEqual(subject, msg.get_decoded_header('subject')) 54 | 55 | # try to handle different timezone carefully 56 | mail_date=list(email.utils.parsedate(msg.get_decoded_header('date'))) 57 | self.assertEqual(mail_date[:6], list(time.localtime(date))[:6]) 58 | 59 | self.assertNotEqual(msg.get('message-id').find(message_id_string), -1) 60 | for name, value in headers: 61 | self.assertEqual(value, msg.get_decoded_header(name)) 62 | 63 | for mailpart in msg.mailparts: 64 | if mailpart.is_body: 65 | self.assertEqual(mailpart.content_id, None) 66 | self.assertEqual(mailpart.filename, None) 67 | self.assertEqual(type(mailpart.sanitized_filename), str) 68 | if mailpart.type=='text/plain': 69 | self.assertEqual(mailpart.get_payload(), text_content.encode(text_encoding)) 70 | else: 71 | self.fail('found unknown body part') 72 | else: 73 | if mailpart.filename: 74 | lst=attachments 75 | self.assertEqual(mailpart.filename, mailpart.sanitized_filename) 76 | self.assertEqual(mailpart.content_id, None) 77 | elif mailpart.content_id: 78 | lst=embeddeds 79 | self.assertEqual(mailpart.filename, None) 80 | else: 81 | self.fail('found unknown part') 82 | 83 | found=False 84 | for attach in lst: 85 | found=(mailpart.filename and attach[3]==mailpart.filename) \ 86 | or (mailpart.content_id and attach[3]==mailpart.content_id) 87 | if found: 88 | break 89 | 90 | if found: 91 | self.assertEqual(mailpart.type, attach[1]+'/'+attach[2]) 92 | payload=mailpart.get_payload() 93 | if attach[1]=='text' and attach[4] and isinstance(attach[0], unicode): 94 | payload=payload.decode(attach[4]) 95 | self.assertEqual(payload, attach[0]) 96 | else: 97 | self.fail('found unknown attachment') 98 | 99 | 100 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/test_generate.py: -------------------------------------------------------------------------------- 1 | import unittest, doctest 2 | import pyzmail 3 | from pyzmail.generate import * 4 | 5 | class TestGenerate(unittest.TestCase): 6 | 7 | def setUp(self): 8 | pass 9 | 10 | def test_format_addresses(self): 11 | """test format_addresse""" 12 | self.assertEqual('foo@example.com', str(format_addresses([ 'foo@example.com', ]))) 13 | self.assertEqual('Foo ', str(format_addresses([ ('Foo', 'foo@example.com'), ]))) 14 | # notice the space around the comma 15 | self.assertEqual('foo@example.com , bar@example.com', str(format_addresses([ 'foo@example.com', 'bar@example.com']))) 16 | # notice the space around the comma 17 | self.assertEqual('Foo , Bar ', str(format_addresses([ ('Foo', 'foo@example.com'), ( 'Bar', 'bar@example.com')]))) 18 | 19 | # Add doctest 20 | def load_tests(loader, tests, ignore): 21 | # this works with python 2.7 and 3.x 22 | tests.addTests(doctest.DocTestSuite(pyzmail.generate)) 23 | return tests 24 | 25 | def additional_tests(): 26 | # Add doctest for python 2.6 and below 27 | if sys.version_info<(2, 7): 28 | return doctest.DocTestSuite(pyzmail.generate) 29 | else: 30 | return unittest.TestSuite() 31 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/test_parse.py: -------------------------------------------------------------------------------- 1 | import unittest, doctest 2 | import pyzmail 3 | from pyzmail.parse import * 4 | 5 | 6 | class Msg: 7 | """mimic a email.Message""" 8 | def __init__(self, value): 9 | self.value=value 10 | 11 | def get_all(self, header_name, default): 12 | if self.value: 13 | return [self.value, ] 14 | else: 15 | return [] 16 | 17 | class TestParse(unittest.TestCase): 18 | 19 | def setUp(self): 20 | pass 21 | 22 | def test_decode_mail_header(self): 23 | """test decode_mail_header()""" 24 | self.assertEqual(decode_mail_header(''), u'') 25 | self.assertEqual(decode_mail_header('hello'), u'hello') 26 | self.assertEqual(decode_mail_header('hello '), u'hello ') 27 | self.assertEqual(decode_mail_header('=?iso-8859-1?q?Courrier_=E8lectronique_Fran=E7ais?='), u'Courrier \xe8lectronique Fran\xe7ais') 28 | self.assertEqual(decode_mail_header('=?utf8?q?Courrier_=C3=A8lectronique_Fran=C3=A7ais?='), u'Courrier \xe8lectronique Fran\xe7ais') 29 | self.assertEqual(decode_mail_header('=?utf-8?b?RnJhbsOnYWlz?='), u'Fran\xe7ais') 30 | self.assertEqual(decode_mail_header('=?iso-8859-1?q?Courrier_=E8lectronique_?= =?utf8?q?Fran=C3=A7ais?='), u'Courrier \xe8lectronique Fran\xe7ais') 31 | self.assertEqual(decode_mail_header('=?iso-8859-1?q?Courrier_=E8lectronique_?= =?utf-8?b?RnJhbsOnYWlz?='), u'Courrier \xe8lectronique Fran\xe7ais') 32 | if sys.version_info >= (3,): 33 | self.assertEqual(decode_mail_header('h_subject_q_iso_8858_1 : =?ISO-8859-1?Q?Fran=E7ais=E20accentu=E9?= !'), u'h_subject_q_iso_8858_1 : Fran\xe7ais\xe20accentu\xe9 !') 34 | else: 35 | # 2.X is a bit buggy and add white spaces 36 | self.assertEqual(decode_mail_header('h_subject_q_iso_8858_1 : =?ISO-8859-1?Q?Fran=E7ais=E20accentu=E9?= !'), u'h_subject_q_iso_8858_1 :Fran\xe7ais\xe20accentu\xe9!') 37 | 38 | def test_get_mail_addresses(self): 39 | """test get_mail_addresses()""" 40 | self.assertEqual([ ('foo@example.com', 'foo@example.com') ], get_mail_addresses(Msg('foo@example.com'), 'to')) 41 | self.assertEqual([ ('Foo', 'foo@example.com'), ], get_mail_addresses(Msg('Foo '), 'to')) 42 | # notice the space around the comma 43 | self.assertEqual([ ('foo@example.com', 'foo@example.com'), ('bar@example.com', 'bar@example.com')], get_mail_addresses(Msg('foo@example.com , bar@example.com'), 'to')) 44 | self.assertEqual([ ('Foo', 'foo@example.com'), ( 'Bar', 'bar@example.com')], get_mail_addresses(Msg('Foo , Bar '), 'to')) 45 | self.assertEqual([ ('Foo', 'foo@example.com'), ('bar@example.com', 'bar@example.com')], get_mail_addresses(Msg('Foo , bar@example.com'), 'to')) 46 | self.assertEqual([ ('Mr Foo', 'foo@example.com'), ('bar@example.com', 'bar@example.com')], get_mail_addresses(Msg('Mr\nFoo , bar@example.com'), 'to')) 47 | 48 | self.assertEqual([ (u'Beno\xeet', 'benoit@example.com')], get_mail_addresses(Msg('=?utf-8?q?Beno=C3=AEt?= '), 'to')) 49 | 50 | # address already encoded into utf8 (bad) 51 | address=u'Ant\xf3nio Foo '.encode('utf8') 52 | if sys.version_info<(3, 0): 53 | self.assertEqual([(u'Ant\ufffd\ufffdnio Foo', 'a.foo@example.com')], get_mail_addresses(Msg(address), 'to')) 54 | else: 55 | # Python 3.2 return header when surrogate characters are used in header 56 | self.assertEqual([(u'Ant??nio Foo', 'a.foo@example.com'), ], get_mail_addresses(Msg(email.header.Header(address, charset=email.charset.UNKNOWN8BIT, header_name='to')), 'to')) 57 | 58 | def test_get_filename(self): 59 | """test get_filename()""" 60 | import email.mime.image 61 | 62 | filename=u'Fran\xe7ais.png' 63 | if sys.version_info<(3, 0): 64 | encoded_filename=filename.encode('iso-8859-1') 65 | else: 66 | encoded_filename=filename 67 | 68 | payload=b'data' 69 | attach=email.mime.image.MIMEImage(payload, 'png') 70 | attach.add_header('Content-Disposition', 'attachment', filename='image.png') 71 | self.assertEqual('image.png', get_filename(attach)) 72 | 73 | attach=email.mime.image.MIMEImage(payload, 'png') 74 | attach.add_header('Content-Disposition', 'attachment', filename=('iso-8859-1', 'fr', encoded_filename)) 75 | self.assertEqual(u'Fran\xe7ais.png', get_filename(attach)) 76 | 77 | attach=email.mime.image.MIMEImage(payload, 'png') 78 | attach.set_param('name', 'image.png') 79 | self.assertEqual('image.png', get_filename(attach)) 80 | 81 | attach=email.mime.image.MIMEImage(payload, 'png') 82 | attach.set_param('name', ('iso-8859-1', 'fr', encoded_filename)) 83 | self.assertEqual(u'Fran\xe7ais.png', get_filename(attach)) 84 | 85 | attach=email.mime.image.MIMEImage(payload, 'png') 86 | attach.add_header('Content-Disposition', 'attachment', filename='image.png') 87 | attach.set_param('name', 'image_wrong.png') 88 | self.assertEqual('image.png', get_filename(attach)) 89 | 90 | def test_get_mailparts(self): 91 | """test get_mailparts()""" 92 | import email.mime.multipart 93 | import email.mime.text 94 | import email.mime.image 95 | msg=email.mime.multipart.MIMEMultipart(boundary='===limit1==') 96 | txt=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 97 | msg.attach(txt) 98 | image=email.mime.image.MIMEImage(b'data', 'png') 99 | image.add_header('Content-Disposition', 'attachment', filename='image.png') 100 | image.add_header('Content-Description', 'the description') 101 | image.add_header('Content-ID', '') 102 | msg.attach(image) 103 | 104 | raw=msg.as_string(unixfrom=False) 105 | expected_raw="""Content-Type: multipart/mixed; boundary="===limit1==" 106 | MIME-Version: 1.0 107 | 108 | --===limit1== 109 | Content-Type: text/plain; charset="us-ascii" 110 | MIME-Version: 1.0 111 | Content-Transfer-Encoding: 7bit 112 | 113 | The text. 114 | --===limit1== 115 | Content-Type: image/png 116 | MIME-Version: 1.0 117 | Content-Transfer-Encoding: base64 118 | Content-Disposition: attachment; filename="image.png" 119 | Content-Description: the description 120 | Content-ID: 121 | 122 | ZGF0YQ== 123 | --===limit1==-- 124 | """ 125 | 126 | if sys.version_info<(3, 0): 127 | expected_raw=expected_raw.replace('','') 128 | else: 129 | expected_raw=expected_raw.replace('','\n') 130 | 131 | self.assertEqual(raw, expected_raw) 132 | 133 | parts=get_mail_parts(msg) 134 | # [MailPart<*text/plain charset=us-ascii len=9>, MailPart] 135 | 136 | self.assertEqual(len(parts), 2) 137 | 138 | self.assertEqual(parts[0].type, 'text/plain') 139 | self.assertEqual(parts[0].is_body, 'text/plain') # not a error, is_body must be type 140 | self.assertEqual(parts[0].charset, 'us-ascii') 141 | self.assertEqual(parts[0].get_payload().decode(parts[0].charset), 'The text.') 142 | 143 | self.assertEqual(parts[1].type, 'image/png') 144 | self.assertEqual(parts[1].is_body, False) 145 | self.assertEqual(parts[1].charset, None) 146 | self.assertEqual(parts[1].filename, 'image.png') 147 | self.assertEqual(parts[1].description, 'the description') 148 | self.assertEqual(parts[1].content_id, 'this.is.the.normaly.unique.contentid') 149 | self.assertEqual(parts[1].get_payload(), b'data') 150 | 151 | 152 | raw_1='''Content-Type: text/plain; charset="us-ascii" 153 | MIME-Version: 1.0 154 | Content-Transfer-Encoding: 7bit 155 | Subject: simple test 156 | From: Me 157 | To: A , B 158 | Cc: C , d@foo.com 159 | User-Agent: pyzmail 160 | 161 | The text. 162 | ''' 163 | 164 | def check_message_1(self, msg): 165 | self.assertEqual(msg.get_subject(), u'simple test') 166 | self.assertEqual(msg.get_decoded_header('subject'), u'simple test') 167 | self.assertEqual(msg.get_decoded_header('User-Agent'), u'pyzmail') 168 | self.assertEqual(msg.get('User-Agent'), 'pyzmail') 169 | self.assertEqual(msg.get_address('from'), (u'Me', 'me@foo.com')) 170 | self.assertEqual(msg.get_addresses('to'), [(u'A', 'a@foo.com'), (u'B', 'b@foo.com')]) 171 | self.assertEqual(msg.get_addresses('cc'), [(u'C', 'c@foo.com'), (u'd@foo.com', 'd@foo.com')]) 172 | self.assertEqual(len(msg.mailparts), 1) 173 | self.assertEqual(msg.text_part, msg.mailparts[0]) 174 | self.assertEqual(msg.html_part, None) 175 | 176 | # use 8bits encoding and 2 different charsets ! python 3.0 & 3.1 are not eable to parse this sample 177 | raw_2=b"""From: sender@domain.com 178 | To: recipient@domain.com 179 | Date: Tue, 7 Jun 2011 16:32:17 +0200 180 | Subject: contains 8bits attachments using different encoding 181 | Content-Type: multipart/mixed; boundary=mixed 182 | 183 | --mixed 184 | Content-Type: text/plain; charset="us-ascii" 185 | MIME-Version: 1.0 186 | Content-Transfer-Encoding: 7bit 187 | 188 | body 189 | --mixed 190 | Content-Type: text/plain; charset="windows-1252" 191 | MIME-Version: 1.0 192 | Content-Transfer-Encoding: 8bit 193 | Content-Disposition: attachment; filename="file1.txt" 194 | 195 | bo\xeete mail = mailbox 196 | --mixed 197 | Content-Type: text/plain; charset="utf-8" 198 | MIME-Version: 1.0 199 | Content-Transfer-Encoding: 8bit 200 | Content-Disposition: attachment; filename="file2.txt" 201 | 202 | bo\xc3\xaete mail = mailbox 203 | --mixed-- 204 | """ 205 | 206 | def check_message_2(self, msg): 207 | self.assertEqual(msg.get_subject(), u'contains 8bits attachments using different encoding') 208 | 209 | body, file1, file2=msg.mailparts 210 | 211 | self.assertEqual('file1.txt', file1.filename) 212 | self.assertEqual('file2.txt', file2.filename) 213 | self.assertEqual('windows-1252', file1.charset) 214 | self.assertEqual('utf-8', file2.charset) 215 | content=b'bo\xeete mail = mailbox'.decode("windows-1252") 216 | content1=file1.get_payload().decode(file1.charset) 217 | content2=file2.get_payload().decode(file2.charset) 218 | self.assertEqual(content, content1) 219 | self.assertEqual(content, content2) 220 | 221 | # this one contain non us-ascii chars in the header 222 | # py 2x and py3k return different value here 223 | raw_3=b'Content-Type: text/plain; charset="us-ascii"\n' \ 224 | b'MIME-Version: 1.0\n' \ 225 | b'Content-Transfer-Encoding: 7bit\n' \ 226 | + u'Subject: Beno\xeet & Ant\xf3nio\n'.encode('utf8') +\ 227 | b'From: =?utf-8?q?Beno=C3=AEt?= \n' \ 228 | + u'To: Ant\xf3nio Foo \n'.encode('utf8') \ 229 | + u'Cc: Beno\xeet , d@foo.com\n'.encode('utf8') +\ 230 | b'User-Agent: pyzmail\n' \ 231 | b'\n' \ 232 | b'The text.\n' 233 | 234 | def check_message_3(self, msg): 235 | subject=u'Beno\ufffd\ufffdt & Ant\ufffd\ufffdnio' # if sys.version_info<(3, 0) else u'Beno??t & Ant??nio' 236 | self.assertEqual(msg.get_subject(), subject) 237 | self.assertEqual(msg.get_decoded_header('subject'), subject) 238 | self.assertEqual(msg.get_decoded_header('User-Agent'), u'pyzmail') 239 | self.assertEqual(msg.get('User-Agent'), 'pyzmail') 240 | self.assertEqual(msg.get_address('from'), (u'Beno\xeet', 'benoit@example.com')) 241 | 242 | to=msg.get_addresses('to') 243 | self.assertEqual(to[0][1], 'a.foo@example.com') 244 | self.assertEqual(to[0][0], u'Ant\ufffd\ufffdnio Foo' if sys.version_info<(3, 0) else u'Ant??nio Foo') 245 | 246 | cc=msg.get_addresses('cc') 247 | self.assertEqual(cc[0][1], 'benoit@foo.com') 248 | self.assertEqual(cc[0][0], u'Beno\ufffd\ufffdt' if sys.version_info<(3, 0) else u'Beno??t') 249 | self.assertEqual(cc[1], ('d@foo.com', 'd@foo.com')) 250 | 251 | self.assertEqual(len(msg.mailparts), 1) 252 | self.assertEqual(msg.text_part, msg.mailparts[0]) 253 | self.assertEqual(msg.html_part, None) 254 | 255 | 256 | def check_pyzmessage_factories(self, input, check): 257 | """test PyzMessage from different sources""" 258 | if isinstance(input, bytes) and sys.version_info>=(3, 2): 259 | check(PyzMessage.factory(input)) 260 | check(message_from_bytes(input)) 261 | 262 | import io 263 | check(PyzMessage.factory(io.BytesIO(input))) 264 | check(message_from_binary_file(io.BytesIO(input))) 265 | 266 | if isinstance(input, basestring): 267 | 268 | check(PyzMessage.factory(input)) 269 | check(message_from_string(input)) 270 | 271 | import StringIO 272 | check(PyzMessage.factory(StringIO.StringIO(input))) 273 | check(message_from_file(StringIO.StringIO(input))) 274 | 275 | def test_pyzmessage_factories(self): 276 | """test PyzMessage class different sources""" 277 | self.check_pyzmessage_factories(self.raw_1, self.check_message_1) 278 | self.check_pyzmessage_factories(self.raw_2, self.check_message_2) 279 | self.check_pyzmessage_factories(self.raw_3, self.check_message_3) 280 | 281 | 282 | # Add doctest 283 | def load_tests(loader, tests, ignore): 284 | # this works with python 2.7 and 3.x 285 | if sys.version_info<(3, 0): 286 | tests.addTests(doctest.DocTestSuite(pyzmail.parse)) 287 | return tests 288 | 289 | def additional_tests(): 290 | # Add doctest for python 2.6 and below 291 | if sys.version_info<(2, 7): 292 | return doctest.DocTestSuite(pyzmail.parse) 293 | else: 294 | return unittest.TestSuite() 295 | 296 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/test_send.py: -------------------------------------------------------------------------------- 1 | import threading, smtpd, asyncore, socket, smtplib, time 2 | import unittest 3 | import pyzmail 4 | from pyzmail.generate import * 5 | 6 | 7 | smtpd_addr='127.0.0.1' 8 | smtpd_port=32525 9 | smtp_bad_port=smtpd_port-1 10 | 11 | smtp_mode='normal' 12 | smtp_login=None 13 | smtp_password=None 14 | 15 | 16 | class SMTPServer(smtpd.SMTPServer): 17 | def __init__(self, localaddr, remoteaddr, received): 18 | smtpd.SMTPServer.__init__(self, localaddr, remoteaddr) 19 | self.set_reuse_addr() 20 | # put the received mail into received list 21 | self.received=received 22 | 23 | def process_message(self, peer, mail_from, rcpt_to, data): 24 | ret=None 25 | if mail_from.startswith('data_error'): 26 | ret='552 Requested mail action aborted: exceeded storage allocation' 27 | self.received.append((ret, peer, mail_from, rcpt_to, data)) 28 | return ret 29 | 30 | class TestSend(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.received=[] 34 | self.smtp_server=SMTPServer((smtpd_addr, smtpd_port), None, self.received) 35 | 36 | def asyncloop(): 37 | # check every sec if all channel are close 38 | asyncore.loop(1) 39 | 40 | 41 | self.payload, self.mail_from, self.rcpt_to, self.msg_id=compose_mail((u'Me', 'me@foo.com'), [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii')) 42 | 43 | # start the server after having built the payload, to handle failure in 44 | # the code above 45 | self.smtpd_thread=threading.Thread(target=asyncloop) 46 | self.smtpd_thread.daemon=True 47 | self.smtpd_thread.start() 48 | time.sleep(0.1) 49 | 50 | 51 | def tearDown(self): 52 | time.sleep(0.1) 53 | self.smtp_server.close() 54 | self.smtpd_thread.join() 55 | 56 | def test_simple_send(self): 57 | """simple send""" 58 | ret=send_mail(self.payload, self.mail_from, self.rcpt_to, smtpd_addr, smtpd_port, smtp_mode=smtp_mode, smtp_login=smtp_login, smtp_password=smtp_password) 59 | self.assertEqual(ret, dict()) 60 | (ret, peer, mail_from, rcpt_to, payload)=self.received[0] 61 | self.assertEqual(self.payload, payload) 62 | self.assertEqual(self.mail_from, mail_from) 63 | self.assertEqual(self.rcpt_to, rcpt_to) 64 | self.assertEqual('127.0.0.1', peer[0]) 65 | 66 | def test_send_to_a_wrong_port(self): 67 | """send to a wrong port""" 68 | ret=send_mail(self.payload, self.mail_from, self.rcpt_to, smtpd_addr, smtp_bad_port, smtp_mode=smtp_mode, smtp_login=smtp_login, smtp_password=smtp_password) 69 | self.assertEqual(type(ret), str) 70 | self.assertTrue('not responding' in ret or '111' in ret or 'Connection refused' in ret) 71 | 72 | def test_send_data_error(self): 73 | """smtp server return error code""" 74 | ret=send_mail(self.payload, 'data_error@foo.com', self.rcpt_to, smtpd_addr, smtpd_port, smtp_mode=smtp_mode, smtp_login=smtp_login, smtp_password=smtp_password) 75 | self.assertEqual(type(ret), str) 76 | self.assertTrue('exceeded storage allocation' in ret) 77 | 78 | if __name__ == '__main__': 79 | unittest.main() 80 | 81 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest, doctest 2 | import pyzmail 3 | from pyzmail.utils import * 4 | 5 | class TestUtils(unittest.TestCase): 6 | 7 | def setUp(self): 8 | pass 9 | 10 | def test_nothing(self): 11 | pass 12 | 13 | # Add doctest 14 | def load_tests(loader, tests, ignore): 15 | # this works with python 2.7 and 3.x 16 | tests.addTests(doctest.DocTestSuite(pyzmail.utils)) 17 | return tests 18 | 19 | def additional_tests(): 20 | # Add doctest for python 2.6 and below 21 | if sys.version_info<(2, 7): 22 | return doctest.DocTestSuite(pyzmail.utils) 23 | else: 24 | return unittest.TestSuite() 25 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # pyzmail/utils.py 3 | # (c) Alain Spineux 4 | # http://www.magiksys.net/pyzmail 5 | # Released under LGPL 6 | 7 | """ 8 | Various functions used by other modules 9 | @var invalid_chars_in_filename: a mix of characters not permitted in most used filesystems 10 | @var invalid_windows_name: a list of unauthorized filenames under Windows 11 | """ 12 | 13 | import sys 14 | 15 | invalid_chars_in_filename=b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' \ 16 | b'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f' \ 17 | b'<>:"/\\|?*\%\'' 18 | 19 | invalid_windows_name=[b'CON', b'PRN', b'AUX', b'NUL', b'COM1', b'COM2', b'COM3', 20 | b'COM4', b'COM5', b'COM6', b'COM7', b'COM8', b'COM9', 21 | b'LPT1', b'LPT2', b'LPT3', b'LPT4', b'LPT5', b'LPT6', b'LPT7', 22 | b'LPT8', b'LPT9' ] 23 | 24 | def sanitize_filename(filename, alt_name, alt_ext): 25 | """ 26 | Convert the given filename into a name that should work on all 27 | platform. Remove non us-ascii characters, and drop invalid filename. 28 | Use the I{alternative} filename if needed. 29 | 30 | @type filename: unicode or None 31 | @param filename: the originale filename or None. Can be unicode. 32 | @type alt_name: str 33 | @param alt_name: the alternative filename if filename is None or useless 34 | @type alt_ext: str 35 | @param alt_ext: the alternative filename extension (including the '.') 36 | 37 | @rtype: str 38 | @returns: a valid filename. 39 | 40 | >>> sanitize_filename('document.txt', 'file', '.txt') 41 | 'document.txt' 42 | >>> sanitize_filename('number1.txt', 'file', '.txt') 43 | 'number1.txt' 44 | >>> sanitize_filename(None, 'file', '.txt') 45 | 'file.txt' 46 | >>> sanitize_filename(u'R\\xe9pertoir.txt', 'file', '.txt') 47 | 'Rpertoir.txt' 48 | >>> # the '\\xe9' has been removed 49 | >>> sanitize_filename(u'\\xe9\\xe6.html', 'file', '.txt') 50 | 'file.html' 51 | >>> # all non us-ascii characters have been removed, the alternative name 52 | >>> # has been used the replace empty string. The originale extention 53 | >>> # is still valid 54 | >>> sanitize_filename(u'COM1.txt', 'file', '.txt') 55 | 'COM1A.txt' 56 | >>> # if name match an invalid name or assimilated then a A is added 57 | """ 58 | 59 | if not filename: 60 | return alt_name+alt_ext 61 | 62 | if ((sys.version_info<(3, 0) and isinstance(filename, unicode)) or \ 63 | (sys.version_info>=(3, 0) and isinstance(filename, str))): 64 | filename=filename.encode('ascii', 'ignore') 65 | 66 | filename=filename.translate(None, invalid_chars_in_filename) 67 | filename=filename.strip() 68 | 69 | upper=filename.upper() 70 | for name in invalid_windows_name: 71 | if upper==name: 72 | filename=filename+b'A' 73 | break 74 | if upper.startswith(name+b'.'): 75 | filename=filename[:len(name)]+b'A'+filename[len(name):] 76 | break 77 | 78 | if sys.version_info>=(3, 0): 79 | # back to string 80 | filename=filename.decode('us-ascii') 81 | 82 | if filename.rfind('.')==0: 83 | filename=alt_name+filename 84 | 85 | return filename 86 | 87 | def handle_filename_collision(filename, filenames): 88 | """ 89 | Avoid filename collision, add a sequence number to the name when required. 90 | 'file.txt' will be renamed into 'file-01.txt' then 'file-02.txt' ... 91 | until their is no more collision. The file is not added to the list. 92 | 93 | Windows don't make the difference between lower and upper case. To avoid 94 | "case" collision, the function compare C{filename.lower()} to the list. 95 | If you provide a list in lower case only, then any collisions will be avoided. 96 | 97 | @type filename: str 98 | @param filename: the filename 99 | @type filenames: list or set 100 | @param filenames: a list of filenames. 101 | 102 | @rtype: str 103 | @returns: the I{filename} or the appropriately I{indexed} I{filename} 104 | 105 | >>> handle_filename_collision('file.txt', [ ]) 106 | 'file.txt' 107 | >>> handle_filename_collision('file.txt', [ 'file.txt' ]) 108 | 'file-01.txt' 109 | >>> handle_filename_collision('file.txt', [ 'file.txt', 'file-01.txt',]) 110 | 'file-02.txt' 111 | >>> handle_filename_collision('foo', [ 'foo',]) 112 | 'foo-01' 113 | >>> handle_filename_collision('foo', [ 'foo', 'foo-01',]) 114 | 'foo-02' 115 | >>> handle_filename_collision('FOO', [ 'foo', 'foo-01',]) 116 | 'FOO-02' 117 | """ 118 | if filename.lower() in filenames: 119 | try: 120 | basename, ext=filename.rsplit('.', 1) 121 | ext='.'+ext 122 | except ValueError: 123 | basename, ext=filename, '' 124 | 125 | i=1 126 | while True: 127 | filename='%s-%02d%s' % (basename, i, ext) 128 | if filename.lower() not in filenames: 129 | break 130 | i+=1 131 | 132 | return filename 133 | 134 | def is_usascii(value): 135 | """" 136 | test if string contains us-ascii characters only 137 | 138 | >>> is_usascii('foo') 139 | True 140 | >>> is_usascii(u'foo') 141 | True 142 | >>> is_usascii(u'Fran\xe7ais') 143 | False 144 | >>> is_usascii('bad\x81') 145 | False 146 | """ 147 | try: 148 | # if value is byte string, it will be decoded first using us-ascii 149 | # and will generate UnicodeEncodeError, this is fine too 150 | value.encode('us-ascii') 151 | except UnicodeError: 152 | return False 153 | 154 | return True 155 | 156 | -------------------------------------------------------------------------------- /pyzmail/pyzmail/version.py: -------------------------------------------------------------------------------- 1 | __version__='1.0.3' 2 | -------------------------------------------------------------------------------- /pyzmail/samples/b_minimal.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: b_minimal: Minimal email 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /pyzmail/samples/h_from_named.eml: -------------------------------------------------------------------------------- 1 | From: Sender 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: b_minimal: Minimal email 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /pyzmail/samples/h_from_q_iso.eml: -------------------------------------------------------------------------------- 1 | From: =?UTF-8?Q?Sender=20of=20mail?= 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: b_minimal: Minimal email 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /pyzmail/samples/h_multi_mixed_recipients.eml: -------------------------------------------------------------------------------- 1 | From: Sender =?UTF-8?Q?Sender=20of=20mail?= 2 | To: Recipient First , =?UTF-8?Q?Recipient=20Second?= , 3 | =?iso-8859-1?B?VHJvaXNp6G1l?= , forth@domain.com 4 | Date: Tue, 7 Jun 2011 16:32:17 +0200 5 | Subject: Multi mixed recipients 6 | 7 | Hello World 8 | 9 | -------------------------------------------------------------------------------- /pyzmail/samples/h_subject_b_utf8.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: h_subject_b_utf8 : =?UTF-8?B?SGVsbG8gV29ybGQ=?= ! 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /pyzmail/samples/h_subject_q_iso_8858_1.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: h_subject_q_iso_8858_1 : =?ISO-8859-1?Q?Fran=E7ais=E20accentu=E9?= ! 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /pyzmail/samples/m_eb_txt_html_png.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: mime includind text and html part 5 | Content-Type: multipart/mixed; boundary=mixed 6 | 7 | --mixed 8 | Content-Type: multipart/related; boundary=related 9 | 10 | --related 11 | Content-Type: multipart/alternative; boundary=alternative 12 | 13 | --alternative 14 | Content-Type: text/plain; charset=ISO-8859-1 15 | 16 | Body text 17 | 18 | --alternative 19 | Content-Type: text/html; charset=ISO-8859-1 20 | 21 | Body HTML
22 | 23 | --alternative-- 24 | --related 25 | Content-Type: image/gif; name="angry.gif" 26 | Content-Disposition: attachment; filename="angry.gif" 27 | Content-Transfer-Encoding: base64 28 | Content-Id: 29 | 30 | R0lGODlhDgAOALMAAAwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA//rMUf8A/wD/ 31 | //Tw5CH5BAAAAAAALAAAAAAOAA4AgwwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA 32 | //rMUf8A/wD///Tw5AQ28B1Gqz3S6jop2sxnAYNGaghAHirQUZh6sEDGPQgy5/b9UI+eZkAkghhG 33 | ZPLIbMKcDMwLhIkAADs= 34 | --related 35 | Content-Type: text/plain; charset=ISO-8859-1 36 | 37 | Related text 38 | 39 | --related 40 | Content-Type: text/html; charset=ISO-8859-1 41 | 42 | 43 | Related HTML
44 | 45 | 46 | --related-- 47 | --mixed 48 | Content-Type: image/png; name="smile.png" 49 | Content-Disposition: attachment; filename="smile.png" 50 | Content-Transfer-Encoding: base64 51 | 52 | iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOBAMAAADtZjDiAAAAMFBMVEUQEAhaUjlaWlp7e3uMezGU 53 | hDGcnJy1lCnGvVretTnn5+/3pSn33mP355T39+//75SdwkyMAAAACXBIWXMAAA7EAAAOxAGVKw4b 54 | AAAAB3RJTUUH2wcJDxEjgefAiQAAAAd0RVh0QXV0aG9yAKmuzEgAAAAMdEVYdERlc2NyaXB0aW9u 55 | ABMJISMAAAAKdEVYdENvcHlyaWdodACsD8w6AAAADnRFWHRDcmVhdGlvbiB0aW1lADX3DwkAAAAJ 56 | dEVYdFNvZnR3YXJlAF1w/zoAAAALdEVYdERpc2NsYWltZXIAt8C0jwAAAAh0RVh0V2FybmluZwDA 57 | G+aHAAAAB3RFWHRTb3VyY2UA9f+D6wAAAAh0RVh0Q29tbWVudAD2zJa/AAAABnRFWHRUaXRsZQCo 58 | 7tInAAAAaElEQVR4nGNYsXv3zt27TzHcPup6XDBmDsOeBvYzLTynGfacuHfm/x8gfS7tbtobEM3w 59 | n2E9kP5n9N/oPZA+//7PP5D8GSCYA6RPzjlzEkSfmTlz+xkgffbkzDlAuvsMWAHDmt0g0AUAmyNE 60 | wLAIvcgAAAAASUVORK5CYII= 61 | --mixed 62 | Content-Type: text/plain; charset=ISO-8859-1 63 | Content-Disposition: attachment; filename="attach.txt" 64 | 65 | Attachment=20text 66 | 67 | --mixed 68 | Content-Type: text/html; charset=ISO-8859-1 69 | Content-Disposition: attachment; filename="attach.html" 70 | 71 | 72 | Related HTML
73 | 74 | 75 | --mixed-- 76 | -------------------------------------------------------------------------------- /pyzmail/samples/m_txt_ebhtml_png.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: mime includind text and html part 5 | Content-Type: multipart/mixed; boundary=mixed 6 | 7 | --mixed 8 | Content-Type: multipart/alternative; boundary=alternative 9 | 10 | --alternative 11 | Content-Type: text/plain; charset=ISO-8859-1 12 | 13 | Hello World 14 | 15 | --alternative 16 | Content-Type: multipart/related; boundary=related 17 | 18 | --related 19 | Content-Type: text/html; charset=ISO-8859-1 20 | 21 | Hello World
22 | 23 |
24 | 25 | --related 26 | Content-Type: image/gif; name="angry.gif" 27 | Content-Disposition: attachment; filename="angry.gif" 28 | Content-Transfer-Encoding: base64 29 | Content-Id: 30 | 31 | R0lGODlhDgAOALMAAAwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA//rMUf8A/wD/ 32 | //Tw5CH5BAAAAAAALAAAAAAOAA4AgwwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA 33 | //rMUf8A/wD///Tw5AQ28B1Gqz3S6jop2sxnAYNGaghAHirQUZh6sEDGPQgy5/b9UI+eZkAkghhG 34 | ZPLIbMKcDMwLhIkAADs= 35 | --related-- 36 | --alternative-- 37 | --mixed 38 | Content-Type: image/png; name="smile.png" 39 | Content-Disposition: attachment; filename="smile.png" 40 | Content-Transfer-Encoding: base64 41 | 42 | iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOBAMAAADtZjDiAAAAMFBMVEUQEAhaUjlaWlp7e3uMezGU 43 | hDGcnJy1lCnGvVretTnn5+/3pSn33mP355T39+//75SdwkyMAAAACXBIWXMAAA7EAAAOxAGVKw4b 44 | AAAAB3RJTUUH2wcJDxEjgefAiQAAAAd0RVh0QXV0aG9yAKmuzEgAAAAMdEVYdERlc2NyaXB0aW9u 45 | ABMJISMAAAAKdEVYdENvcHlyaWdodACsD8w6AAAADnRFWHRDcmVhdGlvbiB0aW1lADX3DwkAAAAJ 46 | dEVYdFNvZnR3YXJlAF1w/zoAAAALdEVYdERpc2NsYWltZXIAt8C0jwAAAAh0RVh0V2FybmluZwDA 47 | G+aHAAAAB3RFWHRTb3VyY2UA9f+D6wAAAAh0RVh0Q29tbWVudAD2zJa/AAAABnRFWHRUaXRsZQCo 48 | 7tInAAAAaElEQVR4nGNYsXv3zt27TzHcPup6XDBmDsOeBvYzLTynGfacuHfm/x8gfS7tbtobEM3w 49 | n2E9kP5n9N/oPZA+//7PP5D8GSCYA6RPzjlzEkSfmTlz+xkgffbkzDlAuvsMWAHDmt0g0AUAmyNE 50 | wLAIvcgAAAAASUVORK5CYII= 51 | --mixed-- 52 | -------------------------------------------------------------------------------- /pyzmail/samples/m_txt_html.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: mime includind text and html part 5 | Content-Type: multipart/alternative; boundary=alternative 6 | 7 | --alternative 8 | Content-Type: text/plain; charset=ISO-8859-1 9 | 10 | Hello World 11 | 12 | --alternative 13 | Content-Type: text/html; charset=ISO-8859-1 14 | 15 | Hello World
16 |
17 | 18 | --alternative-- 19 | -------------------------------------------------------------------------------- /pyzmail/samples/m_txt_html_png.eml: -------------------------------------------------------------------------------- 1 | From: sender@domain.com 2 | To: recipient@domain.com 3 | Date: Tue, 7 Jun 2011 16:32:17 +0200 4 | Subject: mime includind text and html part 5 | Content-Type: multipart/mixed; boundary=mixed 6 | 7 | --mixed 8 | Content-Type: multipart/alternative; boundary=alternative 9 | 10 | --alternative 11 | Content-Type: text/plain; charset=ISO-8859-1 12 | 13 | Hello World 14 | 15 | --alternative 16 | Content-Type: text/html; charset=ISO-8859-1 17 | 18 | Hello World
19 |
20 | 21 | --alternative-- 22 | --mixed 23 | Content-Type: image/png; name="smile.png" 24 | Content-Disposition: attachment; filename="smile.png" 25 | Content-Transfer-Encoding: base64 26 | 27 | iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOBAMAAADtZjDiAAAAMFBMVEUQEAhaUjlaWlp7e3uMezGU 28 | hDGcnJy1lCnGvVretTnn5+/3pSn33mP355T39+//75SdwkyMAAAACXBIWXMAAA7EAAAOxAGVKw4b 29 | AAAAB3RJTUUH2wcJDxEjgefAiQAAAAd0RVh0QXV0aG9yAKmuzEgAAAAMdEVYdERlc2NyaXB0aW9u 30 | ABMJISMAAAAKdEVYdENvcHlyaWdodACsD8w6AAAADnRFWHRDcmVhdGlvbiB0aW1lADX3DwkAAAAJ 31 | dEVYdFNvZnR3YXJlAF1w/zoAAAALdEVYdERpc2NsYWltZXIAt8C0jwAAAAh0RVh0V2FybmluZwDA 32 | G+aHAAAAB3RFWHRTb3VyY2UA9f+D6wAAAAh0RVh0Q29tbWVudAD2zJa/AAAABnRFWHRUaXRsZQCo 33 | 7tInAAAAaElEQVR4nGNYsXv3zt27TzHcPup6XDBmDsOeBvYzLTynGfacuHfm/x8gfS7tbtobEM3w 34 | n2E9kP5n9N/oPZA+//7PP5D8GSCYA6RPzjlzEkSfmTlz+xkgffbkzDlAuvsMWAHDmt0g0AUAmyNE 35 | wLAIvcgAAAAASUVORK5CYII= 36 | --mixed-- 37 | -------------------------------------------------------------------------------- /pyzmail/samples/non_delivery1.eml: -------------------------------------------------------------------------------- 1 | Date: Tue, 21 Jun 2011 17:46:06 +0200 2 | From: Mail Delivery Subsystem 3 | Message-Id: <201106211546.p5LFk6LA016280> 4 | To: 5 | MIME-Version: 1.0 6 | Content-Type: multipart/report; report-type=delivery-status; 7 | boundary="p5LFk6LA016280" 8 | Subject: Returned mail: see transcript for details 9 | 10 | This is a MIME-encapsulated message 11 | 12 | --p5LFk6LA016280.1308671166/mail22.consilium.eu.int 13 | 14 | The original message was received at Tue, 21 Jun 2011 17:34:39 +0200 15 | from ex-fe-02.consilium.eu.int [170.255.63.2] 16 | 17 | ----- The following addresses had permanent fatal errors ----- 18 | 19 | (reason: 554 5.4.6 Too many hops) 20 | 21 | ----- Transcript of session follows ----- 22 | 554 5.4.6 Too many hops 26 (25 max): from via 23 | localhost, to 24 | 25 | --p5LFk6LA016280.1308671166/mail22.consilium.eu.int 26 | Content-Type: message/delivery-status 27 | 28 | Reporting-MTA: dns; mail22.consilium.eu.int 29 | Arrival-Date: Tue, 21 Jun 2011 17:34:39 +0200 30 | 31 | Final-Recipient: RFC822; jean-claude.piris@consilium.europa.eu 32 | Action: failed 33 | Status: 5.4.6 34 | Diagnostic-Code: SMTP; 554 5.4.6 Too many hops 35 | Last-Attempt-Date: Tue, 21 Jun 2011 17:46:06 +0200 36 | 37 | --p5LFk6LA016280.1308671166/mail22.consilium.eu.int 38 | Content-Type: message/rfc822 39 | 40 | Subject: UPDATE! - EPC INVITATION: Comitology after Lisbon: a more 41 | democratic and efficient system? _ 28 June 2011_ 14.00-16.00 42 | Date: Tue, 21 Jun 2011 13:47:21 +0200 43 | Message-ID: <7D454E7C81A5A840AC375C2C631383B9048CC6F6@EPCmail.theepc.be> 44 | X-MS-Has-Attach: yes 45 | X-MS-TNEF-Correlator: 46 | Thread-Topic: UPDATE! - EPC INVITATION: Comitology after Lisbon: a more 47 | democratic and efficient system? _ 28 June 2011_ 14.00-16.00 48 | Thread-Index: AcwwCPrKQPESKqBsQTyZ5bqdN9hjCA== 49 | X-Priority: 1 50 | Priority: Urgent 51 | Importance: high 52 | From: "Corina Stratulat" 53 | Bcc: 54 | X-OriginalArrivalTime: 21 Jun 2011 11:49:28.0860 (UTC) 55 | FILETIME=[46DC8DC0:01CC3009] 56 | 57 | This is a multi-part message in MIME format. 58 | 59 | ------_=_NextPart_001_01CC3008.FB2C609F 60 | Content-Type: multipart/alternative; 61 | boundary="----_=_NextPart_002_01CC3008.FB2C609F" 62 | 63 | ------_=_NextPart_002_01CC3008.FB2C609F 64 | Content-Type: text/plain; 65 | charset="iso-8859-1" 66 | Content-Transfer-Encoding: quoted-printable 67 | 68 | Mail content 69 | 70 | ------_=_NextPart_002_01CC3008.FB2C609F 71 | Content-Type: text/html; 72 | charset="iso-8859-1" 73 | Content-Transfer-Encoding: quoted-printable 74 | 75 | 2 | Envelope-to: , 3 | Cc: 4 | Delivery-date: Fri, 24 May 2013 10:08:53 -0600 5 | Received: from [192.168.0.22] ([192.168.0.72:49206] helo=pimailer) by bad.abc.com (envelope-from ) (ecelerity 2.2.2.45 r(34222M)) with ESMTP id CF/CD-45080-44KAKU1I; Fri, 24 May 2013 10:08:53 -0600 6 | Reply-To: 7 | Bounces_to: abc.1A15KLMF5WAX7L4Z1K068C5DZG4YUAEAL@bad.abc.com 8 | Message-ID: 9 | Date: Fri, 24 May 2013 10:08:53 -0600 10 | From: "abc" 11 | Subject: This is an email 12 | To: , 13 | Cc: 14 | MIME-Version: 1.0 15 | Content-Type: multipart/alternative; boundary="----=_Part_51109_1557446830.5619341964557" 16 | 17 | This is a multi-part message in MIME format. 18 | ------=_Part_51109_1557446830.5619341964557 19 | Content-Type: text/plain; charset=koi8-r; format=flowed 20 | Content-Transfer-Encoding: 8bit 21 | 22 | e-mail part one 23 | 24 | 25 | ------=_Part_51109_1557446830.5619341964557 26 | Content-Type: text/html; charset=koi8-r 27 | Content-Transfer-Encoding: quoted-printable 28 | 29 | 30 | 31 | 32 | head 33 | 34 | 35 | e-mail part one html 36 | 37 | 38 | 39 | 40 | ------=_Part_51109_1557446830.5619341964557 41 | Content-Type: application/zip; 42 | name="email.zip" 43 | Content-Transfer-Encoding: base64 44 | Content-ID: 45 | Content-Disposition: inline; 46 | filename="email.zip" 47 | 48 | 49 | UEsDBAoAAgAAAEdbB0N3TptfHQAAAB0AAAAHABwAYWJjLnR4dFVUCQADFWcCUhVnAlJ1eAsAAQT1 50 | AQAABBQAAAB0aGlzIGlzIGFuIGVtYWlsIGF0dGFjaG1lbnQuClBLAQIeAwoAAgAAAEdbB0N3Tptf 51 | HQAAAB0AAAAHABgAAAAAAAEAAACkgQAAAABhYmMudHh0VVQFAAMVZwJSdXgLAAEE9QEAAAQUAAAA 52 | UEsFBgAAAAABAAEATQAAAF4AAAAAAA== 53 | 54 | ------=_Part_51109_1557446830.5619341964557-- 55 | -------------------------------------------------------------------------------- /pyzmail/samples/test2.eml: -------------------------------------------------------------------------------- 1 | Return-path: <6FQEP9YTQFYNB991CY7UHBM3UD40O7N1@bad.abc.com> 2 | Envelope-to: , 3 | Cc: 4 | Delivery-date: Fri, 24 May 2013 10:08:53 -0600 5 | Received: from [192.168.0.22] ([192.168.0.72:49206] helo=pimailer) by bad.abc.com (envelope-from ) (ecelerity 2.2.2.45 r(34222M)) with ESMTP id CF/CD-45080-44KAKU1I; Fri, 24 May 2013 10:08:53 -0600 6 | Reply-To: 7 | Bounces_to: abc.1A15KLMF5WAX7L4Z1K068C5DZG4YUAEAL@bad.abc.com 8 | Message-ID: 9 | Date: Fri, 24 May 2013 10:08:53 -0600 10 | From: "abc" 11 | Subject: This is an email 12 | To: , 13 | Cc: 14 | MIME-Version: 1.0 15 | Content-Type: multipart/alternative; 16 | boundary="----=_Part_51109_1557446830.5619341964557" 17 | 18 | This is a multi-part message in MIME format. 19 | ------=_Part_51109_1557446830.5619341964557 20 | Content-Type: text/plain; charset=koi8-r; format=flowed 21 | Content-Transfer-Encoding: 8bit 22 | 23 | e-mail part one 24 | 25 | ------=_Part_51109_1557446830.5619341964557 26 | Content-Type: multipart/related; 27 | boundary="----=_Part_51109_1557446830.5619341964557" 28 | 29 | 30 | ------=_Part_51109_1557446830.5619341964557 31 | Content-Type: text/html; charset=koi8-r 32 | Content-Transfer-Encoding: quoted-printable 33 | 34 | 35 | 36 | 37 | head 38 | 39 | 40 | e-mail part one html 41 | 42 | 43 | 44 | ------=_Part_51109_1557446830.5619341964557 45 | 46 | ------=_Part_51109_1557446830.5619341964557 47 | Content-Type: application/zip; 48 | name="email.zip" 49 | Content-Transfer-Encoding: base64 50 | Content-ID: 51 | Content-Disposition: inline; 52 | filename="email.zip" 53 | 54 | ------=_Part_51109_1557446830.5619341964557-- 55 | 56 | UEsDBAoAAgAAAEdbB0N3TptfHQAAAB0AAAAHABwAYWJjLnR4dFVUCQADFWcCUhVnAlJ1eAsAAQT1 57 | AQAABBQAAAB0aGlzIGlzIGFuIGVtYWlsIGF0dGFjaG1lbnQuClBLAQIeAwoAAgAAAEdbB0N3Tptf 58 | HQAAAB0AAAAHABgAAAAAAAEAAACkgQAAAABhYmMudHh0VVQFAAMVZwJSdXgLAAEE9QEAAAQUAAAA 59 | UEsFBgAAAAABAAEATQAAAF4AAAAAAA== 60 | 61 | ------=_Part_51109_1557446830.5619341964557-- 62 | -------------------------------------------------------------------------------- /pyzmail/samples/test_orig.eml: -------------------------------------------------------------------------------- 1 | Return-path: <6FQEP9YTQFYNB991CY7UHBM3UD40O7N1@bad.abc.com> 2 | Envelope-to: , 3 | 4 | Cc: 5 | Delivery-date: Fri, 24 May 2013 10:08:53 -0600 6 | Received: from [192.168.0.22] ([192.168.0.72:49206] helo=pimailer) by bad.abc.com (envelope-from ) (ecelerity 2.2.2.45 r(34222M)) with ESMTP id CF/CD-45080-44KAKU1I; Fri, 24 May 2013 10:08:53 -0600 7 | Reply-To: 8 | Bounces_to: abc.1A15KLMF5WAX7L4Z1K068C5DZG4YUAEAL@bad.abc.com 9 | Message-ID: 10 | Date: Fri, 24 May 2013 10:08:53 -0600 11 | From: "abc" 12 | Subject: This is an email 13 | To: , 14 | 15 | Cc: 16 | MIME-Version: 1.0 17 | Content-Type: multipart/alternative; 18 | boundary="----=_Part_51109_1557446830.5619341964557" 19 | 20 | This is a multi-part message in MIME format. 21 | ------=_Part_51109_1557446830.5619341964557 22 | Content-Type: text/plain; charset=koi8-r; format=flowed 23 | Content-Transfer-Encoding: 8bit 24 | 25 | e-mail part one 26 | 27 | ------=_Part_51109_1557446830.5619341964557 28 | Content-Type: multipart/related; 29 | boundary="----=_Part_51109_1557446830.5619341964557" 30 | 31 | 32 | ------=_Part_51109_1557446830.5619341964557 33 | Content-Type: text/html; charset=koi8-r 34 | Content-Transfer-Encoding: quoted-printable 35 | 36 | 37 | 38 | 39 | head 40 | 41 | 42 | e-mail part one html 43 | 44 | 45 | 46 | ------=_Part_51109_1557446830.5619341964557 47 | 48 | ------=_Part_51109_1557446830.5619341964557 49 | Content-Type: application/zip; 50 | name="email.zip" 51 | Content-Transfer-Encoding: base64 52 | Content-ID: 53 | Content-Disposition: inline; 54 | filename="email.zip" 55 | 56 | ------=_Part_51109_1557446830.5619341964557-- 57 | 58 | UEsDBAoAAgAAAEdbB0N3TptfHQAAAB0AAAAHABwAYWJjLnR4dFVUCQADFWcCUhVnAlJ1eAsAAQT1 59 | AQAABBQAAAB0aGlzIGlzIGFuIGVtYWlsIGF0dGFjaG1lbnQuClBLAQIeAwoAAgAAAEdbB0N3Tptf 60 | HQAAAB0AAAAHABgAAAAAAAEAAACkgQAAAABhYmMudHh0VVQFAAMVZwJSdXgLAAEE9QEAAAQUAAAA 61 | UEsFBgAAAAABAAEATQAAAF4AAAAAAA== 62 | 63 | ------=_Part_51109_1557446830.5619341964557-- 64 | -------------------------------------------------------------------------------- /pyzmail/scripts/pyzinfomail: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | # 3 | # pyzmail/pyzinfomail 4 | # (c) Alain Spineux 5 | # http://www.magiksys.net/pyzmail 6 | # Released under GPL 7 | 8 | """read and display mail info""" 9 | 10 | import sys 11 | import os 12 | import optparse 13 | import locale 14 | 15 | try: 16 | import pyzmail 17 | except ImportError: 18 | if os.path.isdir('../pyzmail'): 19 | sys.path.append(os.path.abspath('..')) 20 | elif os.path.isdir('pyzmail'): 21 | sys.path.append(os.path.abspath('.')) 22 | import pyzmail 23 | 24 | parser=optparse.OptionParser() 25 | parser.set_usage('%prog file.elm\n\n' 26 | '\tread and display email data\n\n') 27 | 28 | (options, args) = parser.parse_args() 29 | 30 | if len(args)<1: 31 | parser.error('no filename') 32 | 33 | filename=args[0] 34 | if not os.path.isfile(filename): 35 | parser.error('file not found: %s' % (filename, )) 36 | 37 | msg=pyzmail.PyzMessage.factory(open(filename, 'rb')) 38 | 39 | print 'Subject: %r' % (msg.get_subject(), ) 40 | print 'From: %r' % (msg.get_address('from'), ) 41 | print 'To: %r' % (msg.get_addresses('to'), ) 42 | print 'Cc: %r' % (msg.get_addresses('cc'), ) 43 | print 'Date: %r' % (msg.get('date', ''), ) 44 | print 'Message-Id: %r' % (msg.get('message-id', ''), ) 45 | 46 | for mailpart in msg.mailparts: 47 | # dont forget to be careful to sanitize 'filename' and be carefull 48 | # for filename collision, to before to save : 49 | print ' %sfilename=%r type=%s charset=%s desc=%s size=%d' % ('*'if mailpart.is_body else ' ', mailpart.filename, mailpart.type, mailpart.charset, mailpart.part.get('Content-Description'), 0 if mailpart.get_payload()==None else len(mailpart.get_payload())) 50 | 51 | if mailpart.is_body=='text/plain': 52 | # print first 3 lines 53 | payload, used_charset=pyzmail.decode_text(mailpart.get_payload(), mailpart.charset, None) 54 | for line in payload.split('\n')[:3]: 55 | # be careful console can be unable to display unicode characters 56 | if line: 57 | print ' >', line 58 | -------------------------------------------------------------------------------- /pyzmail/scripts/pyzsendmail: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | # 3 | # pyzmail/pyzsendmail 4 | # (c) Alain Spineux 5 | # http://www.magiksys.net/pyzmail 6 | # Released under GPL 7 | 8 | """compose and send email""" 9 | 10 | import sys 11 | import os 12 | import optparse 13 | import locale 14 | 15 | __version__='1.0.4' 16 | 17 | try: 18 | import pyzmail 19 | except ImportError: 20 | if os.path.isdir('../pyzmail'): 21 | sys.path.append(os.path.abspath('..')) 22 | elif os.path.isdir('pyzmail'): 23 | sys.path.append(os.path.abspath('.')) 24 | import pyzmail 25 | 26 | class BadEmailAddress(ValueError): 27 | pass 28 | 29 | def handle_addr(addr): 30 | """ 31 | split an address in its name and mail address 32 | 33 | >>> handle_addr('Foo Bar ') 34 | ('Foo Bar', 'foo.bar@example.com') 35 | >>> handle_addr(' Foo Bar < foo.bar@example.com > ') 36 | ('Foo Bar', 'foo.bar@example.com') 37 | >>> handle_addr('') 38 | ('foo.bar@example.com', 'foo.bar@example.com') 39 | >>> handle_addr('foo.bar@example.com') 40 | ('foo.bar@example.com', 'foo.bar@example.com') 41 | >>> handle_addr('foo.barATexample.com') 42 | Traceback (most recent call last): 43 | BadEmailAddress: foo.barATexample.com 44 | >>> handle_addr('Foo Bar foo.bar@example.com') 45 | Traceback (most recent call last): 46 | BadEmailAddress: Foo Bar foo.bar@example.com 47 | >>> handle_addr('Foo Bar >> handle_addr('Foo Bar foo.bar@example.com>') 51 | Traceback (most recent call last): 52 | BadEmailAddress: Foo Bar foo.bar@example.com> 53 | """ 54 | 55 | addr=addr.strip() 56 | pos=addr.rfind('<') 57 | if pos>=0 and addr.endswith('>'): 58 | address=addr[pos+1:-1].strip() 59 | name=addr[:pos].strip() 60 | if not name: 61 | name=address 62 | else: 63 | address=addr.strip() 64 | name=address 65 | 66 | if not pyzmail.email_address_re.match(address): 67 | raise BadEmailAddress, address 68 | 69 | return name, address 70 | 71 | 72 | def handle_content(arg, mail_charset): 73 | """ 74 | handle file content argument 75 | 76 | format: encoding:@filename 77 | encoding:content 78 | return (encoding, content) 79 | """ 80 | 81 | try: 82 | encoding, content=arg.split(':', 1) 83 | except ValueError: 84 | parser.error('Wrong file argument: '+arg) 85 | 86 | if content.startswith('@'): 87 | filename=content[1:] 88 | if filename=='-': 89 | content=sys.stdin.read() 90 | else: 91 | if not os.path.isfile(filename): 92 | parser.error('file not found: '+filename) 93 | try: 94 | content=open(filename, 'rb').read() 95 | except IOError, e: 96 | parser.error(str(e)) 97 | 98 | if not encoding: 99 | encoding=mail_charset 100 | return content, encoding 101 | 102 | def handle_attachment(arg): 103 | """ 104 | handle attachment argument 105 | 106 | format: maintype/subtpe:filename:target_file[:charset] 107 | 108 | return (data, maintype, subtype, filename, charset) 109 | """ 110 | 111 | try: 112 | typ, filename, target=arg.split(':', 2) 113 | 114 | try: 115 | # If the target filename is a Windows path containing a ':', 116 | # the ':' of the charset must be present. The charset can be empty 117 | # if their is 2 ':' then the 1st one is part of the target 118 | driveletter, target, charset=target.split(':', 2) 119 | target=driveletter+':'+target 120 | except ValueError: 121 | try: 122 | target, charset=target.split(':', 1) 123 | except ValueError: 124 | charset=None 125 | except ValueError: 126 | parser.error('Wrong attachment argument: '+arg) 127 | 128 | if not charset: 129 | charset=None 130 | 131 | try: 132 | maintype, subtype=typ.split('/') 133 | except ValueError: 134 | parser.error('Invalid type: '+typ) 135 | 136 | try: 137 | data=open(target, 'rb').read() 138 | except IOError, e: 139 | parser.error(str(e)) 140 | 141 | return data, maintype, subtype, filename, charset 142 | 143 | 144 | def check_addr(value): 145 | try: 146 | ret=handle_addr(value) 147 | except BadEmailAddress: 148 | parser.error('invalid address: '+value) 149 | 150 | return ret 151 | 152 | def check_addresses(values): 153 | ret=[] 154 | for value in values: 155 | try: 156 | ret.append(handle_addr(value)) 157 | except BadEmailAddress: 158 | parser.error('invalid address: '+value) 159 | 160 | return ret 161 | 162 | default_encoding=locale.getdefaultlocale()[1] 163 | if not default_encoding: 164 | # use default per platform 165 | if sys.platform in ('win32', ): 166 | default_encoding='windows-1252' 167 | else: 168 | default_encoding='utf-8' 169 | 170 | def gen_parser(): 171 | parser=optparse.OptionParser() 172 | 173 | parser.add_option("-V", "--version", action="store_true", dest="version", help="display version") 174 | 175 | parser.add_option("-H", "--smtp-host", dest="smtp_host", help="SMTP host relay", metavar="name_or_ip", default='localhost') 176 | parser.add_option("-p", "--smtp-port", dest="smtp_port", help="SMTP port (default=25)", metavar="port", type='int', default='25') 177 | parser.add_option("-L", "--smtp-login", dest="smtp_login", help="SMTP login (if authentication is required)", metavar="login", type='string') 178 | parser.add_option("-P", "--smtp-password", dest="smtp_password", help="SMTP password (if authentication is required)", metavar="password", type='string') 179 | parser.add_option("-m", "--smtp-mode", dest="smtp_mode", help="smtp mode in 'normal', 'ssl', 'tls'. (default='normal')", metavar="mode", type='choice', default='normal', choices=('normal', 'ssl', 'tls')) 180 | 181 | if sys.version_info<=(3, 0): 182 | parser.add_option("-A", "--arg-charset", dest="arg_charset", help="these arguments charset (default=%s)" % (default_encoding, ), metavar="charset", type='string', default=default_encoding) 183 | else: 184 | # how to re-decode the already decoded argument from py3k ? 185 | pass 186 | 187 | parser.add_option("-C", "--mail-charset", dest="mail_charset", help="default charset (default=%s)" % (default_encoding, ), metavar="charset", type='string', default=default_encoding) 188 | 189 | parser.add_option("-f", "--from", dest="sender", help="sender address", metavar="sender", type='string') 190 | parser.add_option("-t", "--to", action="append", dest="to", help="add one recipient address", metavar="recipient", type='string', default=[]) 191 | parser.add_option("-c", "--cc", action="append", dest="cc", help="add one cc address", metavar="recipient", type='string', default=[]) 192 | parser.add_option("-b", "--bcc", action="append", dest="bcc", help="add one bcc address", metavar="recipient", type='string', default=[]) 193 | 194 | parser.add_option("-s", "--subject", dest="subject", help="message subject", metavar="subject", type='string', default='no subject') 195 | 196 | parser.add_option("-T", "--text", dest="text", help="text content '[text_charset]:@filename' or '[text_charset]:content' for example", metavar="text", type='string', default='') 197 | parser.add_option("-M", "--html", dest="html", help="html content '[text_charset]:@filename' or '[text_charset]:content'", metavar="html", type='string', default='') 198 | parser.add_option("-a", "--attach", action="append", dest="attachments", help="add an attachment 'maintype/subtype:filename:target_file[:text_charset]'", metavar="file", type='string', default=[]) 199 | parser.add_option("-e", "--embed", action="append", dest="embeddeds", help="add embedded data 'maintype/subtype:content-id:target_file[:text_charset]'", metavar="file", type='string', default=[]) 200 | parser.add_option("-E", "--eicar", action="store_true", dest="eicar", default=False, help="include eicar virus in attachments") 201 | return parser 202 | 203 | if sys.version_info<=(3, 0): 204 | # first decoding to get the arg_charset and decode the command line arguments 205 | parser=gen_parser() 206 | (options, args) = parser.parse_args() 207 | arg_charset=options.arg_charset 208 | sys_argv=map(lambda x:x.decode(arg_charset), sys.argv[:]) 209 | else: 210 | # py3k does it well 211 | sys_argv=sys.argv[:] 212 | 213 | # use a new parser 214 | parser=gen_parser() 215 | (options, args) = parser.parse_args(sys_argv) 216 | 217 | #import doctest 218 | #doctest.testmod() 219 | 220 | if options.version: 221 | print 'pyzmail version: %s' % (pyzmail.__version__, ) 222 | print 'pyzsendmail version: %s' % (__version__, ) 223 | print 'default arg-charset: %s' % (default_encoding, ) 224 | print 'stdin.encoding: %s' % (sys.stdin.encoding, ) 225 | print 'stdout.encoding: %s' % (sys.stdout.encoding, ) 226 | sys.exit(9) 227 | 228 | # Misc 229 | mail_charset=options.mail_charset 230 | subject=options.subject 231 | smtp_login=options.smtp_login 232 | if smtp_login: 233 | smtp_login=smtp_login.encode('utf-8') 234 | smtp_password=options.smtp_password 235 | if smtp_password: 236 | smtp_password=smtp_password.encode('utf-8') 237 | 238 | # Addresses From, To, Cc and Bcc 239 | if not options.sender: 240 | parser.error('option required: --from') 241 | sender=check_addr(options.sender) 242 | 243 | to=[] 244 | if options.to: 245 | to=check_addresses(options.to) 246 | 247 | cc=[] 248 | if options.cc: 249 | cc=check_addresses(options.cc) 250 | 251 | bcc=[] 252 | if options.bcc: 253 | bcc=check_addresses(options.bcc) 254 | 255 | if not to and not cc and not bcc: 256 | parser.error('no recipient') 257 | 258 | # Content 259 | if options.text: 260 | text=handle_content(options.text, mail_charset) 261 | else: 262 | text=None 263 | 264 | if options.html: 265 | html=handle_content(options.html, mail_charset) 266 | else: 267 | html=None 268 | 269 | # Attachments 270 | # type:filename:target_file:charset 271 | # image/png:file.png:/tmp/file.png: 272 | attachments=[] 273 | for attachment in options.attachments: 274 | attachment=handle_attachment(attachment) 275 | # print attachment[1:] 276 | attachments.append(attachment) 277 | 278 | if options.eicar: 279 | attachments.append( 280 | ('X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*', \ 281 | 'application', 'octet-stream', 'eicar.com', None)) 282 | 283 | embeddeds=[] 284 | for embedded in options.embeddeds: 285 | embedded=handle_attachment(embedded) 286 | # print embedded[1:] 287 | embeddeds.append(embedded) 288 | 289 | payload, mail_from, rcpt_to, msg_id=pyzmail.compose_mail(sender, to, subject, mail_charset, text, html, attachments=attachments, embeddeds=embeddeds, cc=cc, bcc=bcc, message_id_string='pyzsendmail') 290 | ret=pyzmail.send_mail(payload, mail_from, rcpt_to, options.smtp_host, smtp_port=options.smtp_port, smtp_mode=options.smtp_mode, smtp_login=options.smtp_login, smtp_password=options.smtp_password) 291 | 292 | if isinstance(ret, dict): 293 | if ret: 294 | print 'failed recipients:' 295 | for recipient, (code, msg) in ret.iteritems(): 296 | print '%d %s\t%s' % (code, recipient, msg) 297 | sys.exit(1) 298 | else: 299 | sys.exit(0) 300 | else: 301 | print 'error:' 302 | print ret 303 | sys.exit(2) 304 | 305 | -------------------------------------------------------------------------------- /pyzmail/setup.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | # pyzmail/setup.py 3 | # (c) alain.spineux@gmail.com 4 | # http://www.magiksys.net/pyzmail 5 | 6 | import sys 7 | 8 | if sys.version_info >= (3,): 9 | # distribute is required for py3k 10 | from distribute_setup import use_setuptools 11 | use_setuptools() 12 | 13 | import sys, os, shutil 14 | 15 | try: 16 | import setuptools 17 | from setuptools import setup, find_packages 18 | except ImportError: 19 | from distutils.core import setup 20 | 21 | basename='pyzmail' 22 | # retrieve $version 23 | version='' 24 | for line in open('pyzmail/version.py'): 25 | if line.startswith("__version__="): 26 | version=line[13:].rstrip()[:-1] 27 | break 28 | 29 | if not version: 30 | print('!!!!!!!!!!!!!!!!!!!!!! VERSION NOT FOUND !!!!!!!!!!!!!!!!!!!!!!!!!') 31 | sys.exit(1) 32 | 33 | print('VERSION', version) 34 | 35 | try: 36 | from py2exe.build_exe import py2exe 37 | except ImportError: 38 | pass 39 | else: 40 | class build_zip(py2exe): 41 | """This class inherit from py2exe, builds the exe file(s), then creates a ZIP file.""" 42 | def run(self): 43 | 44 | import zipfile 45 | # initialize variables and create appropriate directories in 'buid' directory 46 | # please don't change 'dist_dir' in setup() 47 | 48 | orig_dist_dir=self.dist_dir 49 | self.mkpath(orig_dist_dir) 50 | zip_filename=os.path.join(orig_dist_dir, '%s-%s-win32.zip' % (self.distribution.metadata.name, self.distribution.metadata.version,)) 51 | #zip_filename_last=os.path.join(orig_dist_dir, '%s-%s-win32.zip' % (self.distribution.metadata.name, 'last',)) 52 | bdist_base=self.get_finalized_command('bdist').bdist_base 53 | dist_dir=os.path.join(bdist_base, '%s-%s' % (self.distribution.metadata.name, self.distribution.metadata.version, )) 54 | self.dist_dir=dist_dir 55 | print('dist_dir is', dist_dir) 56 | 57 | # let py2exe do it's work. 58 | py2exe.run(self) 59 | 60 | # remove zipfile if exists 61 | if os.path.exists(zip_filename): 62 | os.unlink(zip_filename) 63 | 64 | # create the zipfile 65 | print('Building zip file', zip_filename) 66 | zfile=zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) 67 | for root, dirs, files in os.walk(dist_dir): 68 | for f in files: 69 | filename=os.path.join(root, f) 70 | zfile.write(filename, os.path.relpath(filename, bdist_base)) 71 | 72 | # zfile.writestr('EGG-INFO/PKG-INFO', 'This file is required by PyPI to allow upload') # but I don't want upload 73 | zfile.close() 74 | # If this file is uploaded, it is used by easy_install or pip as a source file 75 | # self.distribution.dist_files.append(('bdist_dumb', '2.3', zip_filename)) 76 | 77 | #shutil.copyfile(zip_filename, zip_filename_last) 78 | 79 | extra_options = {} 80 | doc_dir='share/doc/%s-%s' % (basename, version) 81 | 82 | cmdclass = {} 83 | data_files=[ ] 84 | 85 | if 'py2exe' in sys.argv and os.name=='nt': 86 | doc_dir='doc' 87 | cmdclass = {"py2exe": build_zip} 88 | py2exe_options = { # 'ascii': True, # exclude encodings 89 | 'packages':[], 90 | 'dll_excludes': ['w9xpopen.exe'], # no support for W98 91 | 'compressed':True, 92 | # 'dist_dir': ???? # !!! DONT CHANGE distdir HERE 93 | # exclude big and unused python packages from the binary 94 | 'excludes': [ 'difflib', 'doctest', 'calendar', 'pdb'], 95 | } 96 | extra_options = { 'console': ['scripts/pyzsendmail', 'scripts/pyzinfomail' ], # list of scripts to convert into console exes 97 | 'windows': [], # list of scripts to convert into gui exes 98 | 'options': { 'py2exe': py2exe_options, } , 99 | } 100 | data_files.append( (doc_dir, [ 'docs/build/html/man/pyzsendmail.html', 'docs/build/html/man/pyzinfomail.html']) ) 101 | if '--single-file' in sys.argv[1:]: 102 | sys.argv.remove('--single-file') 103 | py2exe_options.update({ 'bundle_files': 1, }) 104 | extra_options.update({ 'zipfile': None, }) # don't build a separate zip file with all libraries, put them all in the .exe 105 | 106 | data_files.append( (doc_dir, [ 'README.txt', 'Changelog.txt', 'LICENSE.txt']) ) 107 | 108 | # support for python 3.x with "distribute" 109 | if sys.version_info >= (3,): 110 | # avoid setuptools to report unknown options under python 2.X 111 | extra_options['use_2to3'] = True 112 | # extra_options['convert_2to3_doctests'] = ['src/your/module'] 113 | # extra_options['use_2to3_fixers'] = ['your.fixers' ] 114 | extra_options['install_requires']=['distribute'], # be sure we are using distribute 115 | 116 | setup(name='pyzmail', 117 | version=version, 118 | author='Alain Spineux', 119 | author_email='alain.spineux@gmail.com', 120 | url='http://www.magiksys.net/pyzmail', 121 | keywords= 'email', 122 | # maintainer = 'email', # 123 | description='Python easy mail library, to parse, compose and send emails', 124 | long_description='pyzmail is a high level mail library for Python 2.x & 3.x. ' 125 | 'It provides functions and classes that help to parse, ' 126 | 'compose and send emails. pyzmail exists because their ' 127 | 'is no reasons that handling mails with Python would ' 128 | 'be more difficult than with Outlook or Thunderbird. ' 129 | 'pyzmail hide the difficulties of managing the MIME ' 130 | 'structure and of the encoding/decoding for ' 131 | 'internationalized emails. ' 132 | 'pyzmail is well documented, has a lot of code samples ' 133 | 'and include 2 scripts: pyzsendmail and pyzinfomail', 134 | license='LGPL', 135 | packages=[ 'pyzmail', 'pyzmail.tests' ], 136 | test_suite = 'pyzmail.tests', 137 | scripts=[ 'scripts/pyzsendmail', 'scripts/pyzinfomail' ], 138 | data_files=data_files, 139 | classifiers=["Intended Audience :: Developers", 140 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", 141 | "Operating System :: OS Independent", 142 | "Topic :: Communications :: Email", 143 | "Topic :: System :: Networking", 144 | "Topic :: Internet", 145 | "Intended Audience :: Developers", 146 | "Programming Language :: Python", 147 | "Programming Language :: Python :: 2", 148 | "Programming Language :: Python :: 3", 149 | ], 150 | cmdclass = cmdclass, 151 | **extra_options) 152 | 153 | if 'sdist' in sys.argv and 'upload' in sys.argv: 154 | print("After an upload, don't forget to change 'maintainer' to 'email' to be hight in pypi index") 155 | --------------------------------------------------------------------------------