├── conftest.py
├── yagmail
├── example.html
├── sky.jpg
├── compat.py
├── __init__.py
├── utils.py
├── log.py
├── error.py
├── password.py
├── __main__.py
├── headers.py
├── oauth2.py
├── validate.py
├── message.py
└── sender.py
├── MANIFEST.in
├── setup.cfg
├── docs
├── _static
│ └── icon.png
├── index.rst
├── Makefile
├── make.bat
├── api.rst
├── setup.rst
├── usage.rst
└── conf.py
├── .pyup.yml
├── .coveragerc
├── .travis.yml
├── .gitignore
├── tox.ini
├── tests
└── all_test.py
├── deploy.py
├── LICENSE
├── README.rst
├── setup.py
└── README.md
/conftest.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/yagmail/example.html:
--------------------------------------------------------------------------------
1 |
Bla
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include README.rst
3 | include LICENSE
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_rpm]
2 | doc_files = README.rst
3 |
4 | [wheel]
5 | universal = 1
--------------------------------------------------------------------------------
/yagmail/sky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyvorld/yagmail/master/yagmail/sky.jpg
--------------------------------------------------------------------------------
/docs/_static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyvorld/yagmail/master/docs/_static/icon.png
--------------------------------------------------------------------------------
/yagmail/compat.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | PY3 = sys.version_info[0] == 3
5 | text_type = (str,) if PY3 else (str, unicode)
6 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # autogenerated pyup.io config file
2 | # see https://pyup.io/docs/configuration/ for all available options
3 |
4 | schedule: every week
5 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | def __repr__
5 | raise AssertionError
6 | raise NotImplementedError
7 | if __name__ == .__main__.:
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | matrix:
3 | include:
4 | - python: 3.6
5 | env: TOX_ENV=py36
6 | - python: 2.7.10
7 | env: TOX_ENV=py27
8 | install:
9 | - pip install tox
10 | script:
11 | - tox -e $TOX_ENV
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *#*
3 | *.DS_STORE
4 | *.log
5 | *Data.fs*
6 | *flymake*
7 | dist/*
8 | *egg*
9 | urllist*
10 | build/
11 | __pycache__/
12 | /.Python
13 | /bin/
14 | docs/_build/
15 | docs/_static/*
16 | !docs/_static/icon.png
17 | /include/
18 | /lib/
19 | /pip-selfcheck.json
20 | .tox/
21 | .cache
22 | .coverage
23 | .coverage.*
24 | .coveralls.yml
25 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27,py36
3 |
4 | [testenv]
5 | # If you add a new dep here you probably need to add it in setup.py as well
6 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
7 | setenv =
8 | PYTHON_ENV = dev
9 | deps =
10 | pytest
11 | pytest-cov
12 | coveralls
13 | commands =
14 | py.test --cov ./yagmail
15 | coveralls
16 |
--------------------------------------------------------------------------------
/yagmail/__init__.py:
--------------------------------------------------------------------------------
1 | __project__ = "yagmail"
2 | __version__ = "0.11.213"
3 |
4 | from yagmail.error import YagConnectionClosed
5 | from yagmail.error import YagAddressError
6 | from yagmail.password import register
7 | from yagmail.sender import SMTP
8 | from yagmail.sender import logging
9 | from yagmail.utils import raw
10 | from yagmail.utils import inline
11 |
--------------------------------------------------------------------------------
/yagmail/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class raw(str):
5 | """ Ensure that a string is treated as text and will not receive 'magic'. """
6 |
7 | pass
8 |
9 |
10 | class inline(str):
11 | """ Only needed when wanting to inline an image rather than attach it """
12 |
13 | pass
14 |
15 |
16 | def find_user_home_path():
17 | with open(os.path.expanduser("~/.yagmail")) as f:
18 | return f.read().strip()
19 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. yagmail documentation master file, created by
2 | sphinx-quickstart on Tue Sep 19 19:40:28 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | yagmail
7 | =======
8 | `yagmail` is a GMAIL/SMTP client that aims to
9 | make it as simple as possible to send emails.
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | api
15 | setup
16 | usage
17 |
18 |
19 |
20 | Indices and tables
21 | ==================
22 |
23 | * :ref:`genindex`
24 | * :ref:`modindex`
25 | * :ref:`search`
26 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = yagmail
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=yagmail
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/tests/all_test.py:
--------------------------------------------------------------------------------
1 | """ Testing module for yagmail """
2 |
3 | import itertools
4 | from yagmail import SMTP
5 |
6 |
7 | def get_combinations(yag):
8 | """ Creates permutations of possible inputs """
9 | tos = (None, (yag.user), [yag.user, yag.user],
10 | {yag.user: 'me', yag.user + '1': 'me'})
11 | subjects = ('subj', ['subj'], ['subj', 'subj1'])
12 | contents = (None, ['body'], ['body', 'body1',
13 | 'Text
', u"\u2013
"])
14 | results = []
15 | for row in itertools.product(tos, subjects, contents):
16 | options = {y: z for y, z in zip(['to', 'subject', 'contents'], row)}
17 | options['preview_only'] = True
18 | results.append(options)
19 |
20 | return results
21 |
22 |
23 | def test_one():
24 | """ Tests several versions of allowed input for yagmail """
25 | yag = SMTP(smtp_skip_login=True, soft_email_validation=False)
26 | mail_combinations = get_combinations(yag)
27 | for combination in mail_combinations:
28 | print(yag.send(**combination))
29 |
--------------------------------------------------------------------------------
/deploy.py:
--------------------------------------------------------------------------------
1 | """ File unrelated to the package, except for convenience in deploying """
2 | import re
3 | import sh
4 | import os
5 |
6 | commit_count = sh.git('rev-list', ['--all']).count('\n')
7 |
8 | with open('setup.py') as f:
9 | setup = f.read()
10 |
11 | setup = re.sub("MICRO_VERSION = '[0-9]+'", "MICRO_VERSION = '{}'".format(commit_count), setup)
12 |
13 | major = re.search("MAJOR_VERSION = '([0-9]+)'", setup).groups()[0]
14 | minor = re.search("MINOR_VERSION = '([0-9]+)'", setup).groups()[0]
15 | micro = re.search("MICRO_VERSION = '([0-9]+)'", setup).groups()[0]
16 | version = '{}.{}.{}'.format(major, minor, micro)
17 |
18 | with open('setup.py', 'w') as f:
19 | f.write(setup)
20 |
21 | with open('yagmail/__init__.py') as f:
22 | init = f.read()
23 |
24 | with open('yagmail/__init__.py', 'w') as f:
25 | f.write(
26 | re.sub('__version__ = "[0-9.]+"',
27 | '__version__ = "{}"'.format(version), init))
28 |
29 | py_version = "python3.7" if sh.which("python3.7") is not None else "python"
30 | os.system('{} setup.py sdist bdist_wheel upload'.format(py_version))
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =============
3 | This page displays a full reference of `yagmail`\'s API.
4 |
5 |
6 | Authentication
7 | --------------
8 |
9 | .. autofunction:: yagmail.register
10 |
11 | Another way of authenticating is by passing an ``oauth2_file`` to
12 | :class:`yagmail.SMTP`, which is among the safest methods of authentication.
13 | Please see the `OAuth2 section `_
14 | of the `README `_
15 | for further details.
16 |
17 | It is also possible to simply pass the password to :class:`yagmail.SMTP`.
18 | If no password is given, yagmail will prompt the user for a password and
19 | then store the result in the keyring.
20 |
21 |
22 | SMTP Client
23 | -----------
24 | .. autoclass:: yagmail.SMTP
25 | :members:
26 |
27 |
28 | E-Mail Contents
29 | ---------------
30 | .. autoclass:: yagmail.raw
31 |
32 | .. autoclass:: yagmail.inline
33 |
34 |
35 | Exceptions
36 | ----------
37 | .. automodule:: yagmail.error
38 | :members:
39 |
40 |
41 | Utilities
42 | -----------------
43 | .. autofunction:: yagmail.validate.validate_email_with_regex
44 |
--------------------------------------------------------------------------------
/yagmail/log.py:
--------------------------------------------------------------------------------
1 | """
2 | The logging options for yagmail. Note that the logger is set on the SMTP class.
3 |
4 | The default is to only log errors. If wanted, it is possible to do logging with:
5 |
6 | yag = SMTP()
7 | yag.setLog(log_level = logging.DEBUG)
8 |
9 | Furthermore, after creating a SMTP object, it is possible to overwrite and use your own logger by:
10 |
11 | yag = SMTP()
12 | yag.log = myOwnLogger
13 | """
14 |
15 | import logging
16 |
17 |
18 | def get_logger(log_level=logging.DEBUG, file_path_name=None):
19 |
20 | # create logger
21 | logger = logging.getLogger(__name__)
22 |
23 | logger.setLevel(logging.ERROR)
24 |
25 | # create console handler and set level to debug
26 | if file_path_name:
27 | ch = logging.FileHandler(file_path_name)
28 | elif log_level is None:
29 | logger.handlers = [logging.NullHandler()]
30 | return logger
31 | else:
32 | ch = logging.StreamHandler()
33 |
34 | ch.setLevel(log_level)
35 |
36 | # create formatter
37 | formatter = logging.Formatter(
38 | "%(asctime)s [yagmail] [%(levelname)s] : %(message)s", "%Y-%m-%d %H:%M:%S"
39 | )
40 |
41 | # add formatter to ch
42 | ch.setFormatter(formatter)
43 |
44 | # add ch to logger
45 | logger.handlers = [ch]
46 |
47 | return logger
48 |
--------------------------------------------------------------------------------
/yagmail/error.py:
--------------------------------------------------------------------------------
1 | """Contains the exceptions"""
2 |
3 |
4 | class YagConnectionClosed(Exception):
5 |
6 | """
7 | The connection object has been closed by the user.
8 | This object can be used to send emails again after logging in,
9 | using self.login().
10 | """
11 | pass
12 |
13 |
14 | class YagAddressError(Exception):
15 |
16 | """
17 | This means that the address was given in an invalid format.
18 | Note that From can either be a string, or a dictionary where the key is an email,
19 | and the value is an alias {'sample@gmail.com', 'Sam'}. In the case of 'to',
20 | it can either be a string (email), a list of emails (email addresses without aliases)
21 | or a dictionary where keys are the email addresses and the values indicate the aliases.
22 | Furthermore, it does not do any validation of whether an email exists.
23 | """
24 | pass
25 |
26 |
27 | class YagInvalidEmailAddress(Exception):
28 |
29 | """
30 | Note that this will only filter out syntax mistakes in emailaddresses.
31 | If a human would think it is probably a valid email, it will most likely pass.
32 | However, it could still very well be that the actual emailaddress has simply
33 | not be claimed by anyone (so then this function fails to devalidate).
34 | """
35 | pass
36 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | yagmail - Yet another GMAIL / SMTP client
2 | =========================================
3 | `yagmail` is a GMAIL/SMTP client that aims to
4 | make it as simple as possible to send emails.
5 |
6 | Sending an Email is as simple:
7 |
8 | .. code-block:: python
9 |
10 | import yagmail
11 | yag = yagmail.SMTP()
12 | contents = [
13 | "This is the body, and here is just text http://somedomain/image.png",
14 | "You can find an audio file attached.", '/local/path/to/song.mp3'
15 | ]
16 | yag.send('to@someone.com', 'subject', contents)
17 |
18 | # Alternatively, with a simple one-liner:
19 | yagmail.SMTP('mygmailusername').send('to@someone.com', 'subject', contents)
20 |
21 | Note that yagmail will read the password securely from
22 | your keyring, see the section on
23 | `Username and Password in the repository's README
24 | `_
25 | for further details. If you do not want this, you can
26 | initialize ``yagmail.SMTP`` like this:
27 |
28 | .. code-block:: python
29 |
30 | yag = yagmail.SMTP('mygmailusername', 'mygmailpassword')
31 |
32 | but honestly, do you want to have your
33 | password written in your script?
34 |
35 | For further documentation and examples,
36 | please go to https://github.com/kootenpv/yagmail.
37 |
38 | The full documentation is available at
39 | http://yagmail.readthedocs.io/en/latest/.
40 |
--------------------------------------------------------------------------------
/yagmail/password.py:
--------------------------------------------------------------------------------
1 | try:
2 | import keyring
3 | except (ImportError, NameError, RuntimeError):
4 | pass
5 |
6 |
7 | def handle_password(user, password):
8 | """ Handles getting the password"""
9 | if password is None:
10 | try:
11 | password = keyring.get_password("yagmail", user)
12 | except NameError as e:
13 | print(
14 | "'keyring' cannot be loaded. Try 'pip install keyring' or continue without. See https://github.com/kootenpv/yagmail"
15 | )
16 | raise e
17 | if password is None:
18 | import getpass
19 |
20 | password = getpass.getpass("Password for <{0}>: ".format(user))
21 | answer = ""
22 | # Python 2 fix
23 | while answer != "y" and answer != "n":
24 | prompt_string = "Save username and password in keyring? [y/n]: "
25 | # pylint: disable=undefined-variable
26 | try:
27 | answer = raw_input(prompt_string).strip()
28 | except NameError:
29 | answer = input(prompt_string).strip()
30 | if answer == "y":
31 | register(user, password)
32 | return password
33 |
34 |
35 | def register(username, password):
36 | """ Use this to add a new gmail account to your OS' keyring so it can be used in yagmail """
37 | keyring.set_password("yagmail", username, password)
38 |
--------------------------------------------------------------------------------
/yagmail/__main__.py:
--------------------------------------------------------------------------------
1 | from yagmail.sender import SMTP
2 | import sys
3 |
4 | try:
5 | import keyring
6 | except (ImportError, NameError, RuntimeError):
7 | pass
8 |
9 |
10 | def register(username, password):
11 | """ Use this to add a new gmail account to your OS' keyring so it can be used in yagmail """
12 | keyring.set_password('yagmail', username, password)
13 |
14 |
15 | def main():
16 | """ This is the function that is run from commandline with `yagmail` """
17 | import argparse
18 | parser = argparse.ArgumentParser(
19 | description='Send a (g)mail with yagmail.')
20 | subparsers = parser.add_subparsers(dest="command")
21 | oauth = subparsers.add_parser('oauth')
22 | oauth.add_argument('--user', '-u', required=True,
23 | help='The gmail username to register oauth2 for')
24 | oauth.add_argument('--file', '-f', required=True,
25 | help='The filepath to store the oauth2 credentials')
26 | parser.add_argument(
27 | '-to', '-t', help='Send an email to address "TO"', nargs='+')
28 | parser.add_argument('-subject', '-s', help='Subject of email', nargs='+')
29 | parser.add_argument('-contents', '-c', help='Contents to send', nargs='+')
30 | parser.add_argument('-attachments', '-a', help='Attachments to attach', nargs='+')
31 | parser.add_argument('-user', '-u', help='Username')
32 | parser.add_argument('-oauth2', '-o', help='OAuth2 file path')
33 | parser.add_argument(
34 | '-password', '-p',
35 | help='Preferable to use keyring rather than password here')
36 | args = parser.parse_args()
37 | args.contents = args.contents or sys.stdin.read() if not sys.stdin.isatty() else None
38 | if args.command == "oauth":
39 | user = args.user
40 | SMTP(args.user, oauth2_file=args.file)
41 | print("Succesful.")
42 | else:
43 | yag = SMTP(args.user, args.password, oauth2_file=args.oauth2)
44 | yag.send(to=args.to, subject=args.subject,
45 | contents=args.contents, attachments=args.attachments)
46 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | from setuptools import find_packages
3 |
4 | with open('README.rst') as f:
5 | LONG_DESCRIPTION = f.read()
6 | MAJOR_VERSION = '0'
7 | MINOR_VERSION = '11'
8 | MICRO_VERSION = '213'
9 | VERSION = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION)
10 |
11 | setup(name='yagmail',
12 | version=VERSION,
13 | description='Yet Another GMAIL client',
14 | long_description=LONG_DESCRIPTION,
15 | url='https://github.com/kootenpv/yagmail',
16 | author='Pascal van Kooten',
17 | author_email='kootenpv@gmail.com',
18 | license='MIT',
19 | extras_require={
20 | "all": ["keyring"]
21 | },
22 | keywords='email mime automatic html attachment',
23 | entry_points={
24 | 'console_scripts': ['yagmail = yagmail.__main__:main']
25 | },
26 | classifiers=[
27 | 'Environment :: Console',
28 | 'Intended Audience :: Developers',
29 | 'Intended Audience :: Customer Service',
30 | 'Intended Audience :: System Administrators',
31 | 'License :: OSI Approved :: MIT License',
32 | 'Operating System :: Microsoft',
33 | 'Operating System :: MacOS :: MacOS X',
34 | 'Operating System :: Unix',
35 | 'Operating System :: POSIX',
36 | 'Programming Language :: Python',
37 | 'Programming Language :: Python :: 2.7',
38 | 'Programming Language :: Python :: 3',
39 | 'Programming Language :: Python :: 3.3',
40 | 'Programming Language :: Python :: 3.4',
41 | 'Programming Language :: Python :: 3.5',
42 | 'Topic :: Communications :: Email',
43 | 'Topic :: Communications :: Email :: Email Clients (MUA)',
44 | 'Topic :: Software Development',
45 | 'Topic :: Software Development :: Build Tools',
46 | 'Topic :: Software Development :: Debuggers',
47 | 'Topic :: Software Development :: Libraries',
48 | 'Topic :: Software Development :: Libraries :: Python Modules',
49 | 'Topic :: System :: Software Distribution',
50 | 'Topic :: System :: Systems Administration',
51 | 'Topic :: Utilities'
52 | ],
53 | packages=find_packages(),
54 | zip_safe=False,
55 | platforms='any')
56 |
--------------------------------------------------------------------------------
/yagmail/headers.py:
--------------------------------------------------------------------------------
1 | from yagmail.compat import text_type
2 | from yagmail.error import YagAddressError
3 |
4 |
5 | def resolve_addresses(user, useralias, to, cc, bcc):
6 | """ Handle the targets addresses, adding aliases when defined """
7 | addresses = {"recipients": []}
8 | if to is not None:
9 | make_addr_alias_target(to, addresses, "To")
10 | elif cc is not None and bcc is not None:
11 | make_addr_alias_target([user, useralias], addresses, "To")
12 | else:
13 | addresses["recipients"].append(user)
14 | if cc is not None:
15 | make_addr_alias_target(cc, addresses, "Cc")
16 | if bcc is not None:
17 | make_addr_alias_target(bcc, addresses, "Bcc")
18 | return addresses
19 |
20 |
21 | def make_addr_alias_user(email_addr):
22 | if isinstance(email_addr, text_type):
23 | if "@" not in email_addr:
24 | email_addr += "@gmail.com"
25 | return (email_addr, email_addr)
26 | if isinstance(email_addr, dict):
27 | if len(email_addr) == 1:
28 | return (list(email_addr.keys())[0], list(email_addr.values())[0])
29 | raise YagAddressError
30 |
31 |
32 | def make_addr_alias_target(x, addresses, which):
33 | if isinstance(x, text_type):
34 | addresses["recipients"].append(x)
35 | addresses[which] = x
36 | elif isinstance(x, list) or isinstance(x, tuple):
37 | if not all([isinstance(k, text_type) for k in x]):
38 | raise YagAddressError
39 | addresses["recipients"].extend(x)
40 | addresses[which] = "; ".join(x)
41 | elif isinstance(x, dict):
42 | addresses["recipients"].extend(x.keys())
43 | addresses[which] = "; ".join(x.values())
44 | else:
45 | raise YagAddressError
46 |
47 |
48 | def add_subject(msg, subject):
49 | if not subject:
50 | return
51 | if isinstance(subject, list):
52 | subject = " ".join(subject)
53 | msg["Subject"] = subject
54 |
55 |
56 | def add_recipients_headers(user, useralias, msg, addresses):
57 | # Quoting the useralias so it should match display-name from https://tools.ietf.org/html/rfc5322 ,
58 | # even if it's an email address.
59 | msg["From"] = '"{0}" <{1}>'.format(useralias.replace("\\", "\\\\").replace('"', '\\"'), user)
60 | if "To" in addresses:
61 | msg["To"] = addresses["To"]
62 | else:
63 | msg["To"] = useralias
64 | if "Cc" in addresses:
65 | msg["Cc"] = addresses["Cc"]
66 |
--------------------------------------------------------------------------------
/docs/setup.rst:
--------------------------------------------------------------------------------
1 | Setup
2 | =====
3 | This page shows you how to install ``yagmail`` and
4 | how to set it up to use your system keyring service.
5 |
6 |
7 | Installing from PyPI
8 | --------------------
9 | The usual way of installing ``yagmail`` is through PyPI.
10 | It is recommended to install it together with the ``keyring``
11 | library, by running the following (for Python 2.x and 3.x respectively)::
12 |
13 | pip install yagmail[all]
14 | pip3 install yagmail[all]
15 |
16 | If installing ``yagmail`` with ``keyring`` causes issues,
17 | omit the ``[all]`` to install it without.
18 |
19 |
20 | Installing from GitHub
21 | ----------------------
22 | If you're not scared of things occasionally breaking, you can also
23 | install directly from the GitHub `repository `_.
24 | You can do this by running the following (for Python 2.x and 3.x respectively)::
25 |
26 | pip install -e git+https://github.com/kootenpv/yagmail#egg=yagmail[all]
27 | pip3 install -e git+https://github.com/kootenpv/yagmail#egg=yagmail[all]
28 |
29 | Just like with the PyPI installation method, if installing with ``keyring``
30 | causes issues, simply omit the ``[all]`` to install ``yagmail`` without it.
31 |
32 | .. _configuring_credentials:
33 |
34 | Configuring Credentials
35 | -----------------------
36 | While it's possible to put the username and password for your
37 | E-Mail address into your script, ``yagmail`` enables you to omit both.
38 | Quoting from ``keyring``\s `README `_::
39 |
40 | What is Python keyring lib?
41 |
42 | The Python keyring lib provides a easy way to access the system
43 | keyring service from python. It can be used in any
44 | application that needs safe password storage.
45 |
46 | If this sparked your interest, set up a Python interpreter and run
47 | the following to register your GMail credentials with ``yagmail``:
48 |
49 | .. code-block:: python
50 |
51 | import yagmail
52 | yagmail.register('mygmailusername', 'mygmailpassword')
53 |
54 | (this is just a wrapper for ``keyring.set_password('yagmail', 'mygmailusername', 'mygmailpassword')``)
55 | Now, instantiating :class:`yagmail.SMTP` is as easy as doing:
56 |
57 | .. code-block:: python
58 |
59 | yag = yagmail.SMTP('mygmailusername')
60 |
61 | If you want to also omit your username, you can create a ``.yagmail``
62 | file in your home folder, containing just your username. Then, you can
63 | instantiate the SMTP client without passing any arguments.
64 |
65 |
66 | Using OAuth2
67 | ------------
68 | Another fairly safe method for authenticating using OAuth2, since
69 | you can revoke the rights of tokens. In order to use OAuth2, pass
70 | the location of the credentials file to :class:`yagmail.SMTP`:
71 |
72 | .. code-block:: python
73 |
74 | yag = yagmail.SMTP('user@gmail.com', oauth2_file='~/oauth2_creds.json')
75 | yag.send(subject="Great!")
76 |
77 | If the file could not be found, then it will prompt for a
78 | ``google_client_id`` and ``google_client_secret``. You can obtain these
79 | on `this OAauth2 Guide `_,
80 | upon which the OAauth2 code of ``yagmail`` is heavily based.
81 | After you have provided these, a link will be shown in the terminal that
82 | you should follow to obtain a ``google_refresh_token``.
83 | Paste this again, and you're set up!
84 |
85 | If somebody obtains the file, they can send E-Mails, but nothing else.
86 | As soon as you notice, you can simply disable the token.
--------------------------------------------------------------------------------
/yagmail/oauth2.py:
--------------------------------------------------------------------------------
1 | """
2 | Adapted from:
3 | http://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html
4 |
5 | 1. Generate and authorize an OAuth2 (generate_oauth2_token)
6 | 2. Generate a new access tokens using a refresh token(refresh_token)
7 | 3. Generate an OAuth2 string to use for login (access_token)
8 | """
9 | import os
10 | import base64
11 | import json
12 | import getpass
13 |
14 | try:
15 | from urllib.parse import urlencode, quote, unquote
16 | from urllib.request import urlopen
17 | except ImportError:
18 | from urllib import urlencode, quote, unquote, urlopen
19 |
20 | try:
21 | input = raw_input
22 | except NameError:
23 | pass
24 |
25 | GOOGLE_ACCOUNTS_BASE_URL = 'https://accounts.google.com'
26 | REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
27 |
28 |
29 | def command_to_url(command):
30 | return '%s/%s' % (GOOGLE_ACCOUNTS_BASE_URL, command)
31 |
32 |
33 | def url_format_params(params):
34 | param_fragments = []
35 | for param in sorted(params.items(), key=lambda x: x[0]):
36 | escaped_url = quote(param[1], safe='~-._')
37 | param_fragments.append('%s=%s' % (param[0], escaped_url))
38 | return '&'.join(param_fragments)
39 |
40 |
41 | def generate_permission_url(client_id):
42 | params = {}
43 | params['client_id'] = client_id
44 | params['redirect_uri'] = REDIRECT_URI
45 | params['scope'] = 'https://mail.google.com/'
46 | params['response_type'] = 'code'
47 | return '%s?%s' % (command_to_url('o/oauth2/auth'), url_format_params(params))
48 |
49 |
50 | def call_authorize_tokens(client_id, client_secret, authorization_code):
51 | params = {}
52 | params['client_id'] = client_id
53 | params['client_secret'] = client_secret
54 | params['code'] = authorization_code
55 | params['redirect_uri'] = REDIRECT_URI
56 | params['grant_type'] = 'authorization_code'
57 | request_url = command_to_url('o/oauth2/token')
58 | encoded_params = urlencode(params).encode('UTF-8')
59 | response = urlopen(request_url, encoded_params).read().decode('UTF-8')
60 | return json.loads(response)
61 |
62 |
63 | def call_refresh_token(client_id, client_secret, refresh_token):
64 | params = {}
65 | params['client_id'] = client_id
66 | params['client_secret'] = client_secret
67 | params['refresh_token'] = refresh_token
68 | params['grant_type'] = 'refresh_token'
69 | request_url = command_to_url('o/oauth2/token')
70 | encoded_params = urlencode(params).encode('UTF-8')
71 | response = urlopen(request_url, encoded_params).read().decode('UTF-8')
72 | return json.loads(response)
73 |
74 |
75 | def generate_oauth2_string(username, access_token, as_base64=False):
76 | auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
77 | if as_base64:
78 | auth_string = base64.b64encode(auth_string.encode('ascii')).decode('ascii')
79 | return auth_string
80 |
81 |
82 | def get_authorization(google_client_id, google_client_secret):
83 | permission_url = generate_permission_url(google_client_id)
84 | print('Navigate to the following URL to auth:\n' + permission_url)
85 | authorization_code = input('Enter verification code: ')
86 | response = call_authorize_tokens(google_client_id, google_client_secret, authorization_code)
87 | return response['refresh_token'], response['access_token'], response['expires_in']
88 |
89 |
90 | def refresh_authorization(google_client_id, google_client_secret, google_refresh_token):
91 | response = call_refresh_token(google_client_id, google_client_secret, google_refresh_token)
92 | return response['access_token'], response['expires_in']
93 |
94 |
95 | def get_oauth_string(user, oauth2_info):
96 | access_token, expires_in = refresh_authorization(**oauth2_info)
97 | auth_string = generate_oauth2_string(user, access_token, as_base64=True)
98 | return auth_string
99 |
100 |
101 | def get_oauth2_info(oauth2_file):
102 | oauth2_file = os.path.expanduser(oauth2_file)
103 | if os.path.isfile(oauth2_file):
104 | with open(oauth2_file) as f:
105 | oauth2_info = json.load(f)
106 | else:
107 | print("If you do not have an app registered for your email sending purposes, visit:")
108 | print("https://console.developers.google.com")
109 | print("and create a new project.\n")
110 | email_addr = getpass.getpass("Your 'email address': ")
111 | google_client_id = getpass.getpass("Your 'google_client_id': ")
112 | google_client_secret = getpass.getpass("Your 'google_client_secret': ")
113 | google_refresh_token, _, _ = get_authorization(google_client_id, google_client_secret)
114 | oauth2_info = {"email_address": email_addr,
115 | "google_client_id": google_client_id.strip(),
116 | "google_client_secret": google_client_secret.strip(),
117 | "google_refresh_token": google_refresh_token.strip()}
118 | with open(oauth2_file, "w") as f:
119 | json.dump(oauth2_info, f)
120 | return oauth2_info
121 |
--------------------------------------------------------------------------------
/yagmail/validate.py:
--------------------------------------------------------------------------------
1 | """ Module for validating emails.
2 | "Forked" only the regexp part from the "validate_email", see copyright below.
3 | The reason is that if you plan on sending out loads of emails or
4 | doing checks can actually get you blacklisted, if it would be reliable at all.
5 | However, this regexp is the best one I've come accross, so props to Syrus Akbary.
6 | """
7 |
8 | # -----------------------------------------------------------------------------------------------
9 |
10 | # RFC 2822 - style email validation for Python
11 | # (c) 2012 Syrus Akbary
12 | # Extended from (c) 2011 Noel Bush
13 | # for support of mx and user check
14 | # This code is made available to you under the GNU LGPL v3.
15 | #
16 | # This module provides a single method, valid_email_address(),
17 | # which returns True or False to indicate whether a given address
18 | # is valid according to the 'addr-spec' part of the specification
19 | # given in RFC 2822. Ideally, we would like to find this
20 | # in some other library, already thoroughly tested and well-
21 | # maintained. The standard Python library email.utils
22 | # contains a parse_addr() function, but it is not sufficient
23 | # to detect many malformed addresses.
24 | #
25 | # This implementation aims to be faithful to the RFC, with the
26 | # exception of a circular definition (see comments below), and
27 | # with the omission of the pattern components marked as "obsolete".
28 |
29 | import re
30 |
31 |
32 | try:
33 | from .error import YagInvalidEmailAddress
34 | except (ValueError, SystemError):
35 | # stupid fix to make it easy to load interactively
36 | from error import YagInvalidEmailAddress
37 |
38 | # All we are really doing is comparing the input string to one
39 | # gigantic regular expression. But building that regexp, and
40 | # ensuring its correctness, is made much easier by assembling it
41 | # from the "tokens" defined by the RFC. Each of these tokens is
42 | # tested in the accompanying unit test file.
43 | #
44 | # The section of RFC 2822 from which each pattern component is
45 | # derived is given in an accompanying comment.
46 | #
47 | # (To make things simple, every string below is given as 'raw',
48 | # even when it's not strictly necessary. This way we don't forget
49 | # when it is necessary.)
50 | #
51 | # see 2.2.2. Structured Header Field Bodies
52 | WSP = r'[ \t]'
53 | # see 2.2.3. Long Header Fields
54 | CRLF = r'(?:\r\n)'
55 | # see 3.2.1. Primitive Tokens
56 | NO_WS_CTL = r'\x01-\x08\x0b\x0c\x0f-\x1f\x7f'
57 | # see 3.2.2. Quoted characters
58 | QUOTED_PAIR = r'(?:\\.)'
59 | FWS = r'(?:(?:' + WSP + r'*' + CRLF + r')?' + \
60 | WSP + \
61 | r'+)' # see 3.2.3. Folding white space and comments
62 | CTEXT = r'[' + NO_WS_CTL + \
63 | r'\x21-\x27\x2a-\x5b\x5d-\x7e]' # see 3.2.3
64 | CCONTENT = r'(?:' + CTEXT + r'|' + \
65 | QUOTED_PAIR + \
66 | r')' # see 3.2.3 (NB: The RFC includes COMMENT here
67 | # as well, but that would be circular.)
68 | COMMENT = r'\((?:' + FWS + r'?' + CCONTENT + \
69 | r')*' + FWS + r'?\)' # see 3.2.3
70 | CFWS = r'(?:' + FWS + r'?' + COMMENT + ')*(?:' + \
71 | FWS + '?' + COMMENT + '|' + FWS + ')' # see 3.2.3
72 | ATEXT = r'[\w!#$%&\'\*\+\-/=\?\^`\{\|\}~]' # see 3.2.4. Atom
73 | ATOM = CFWS + r'?' + ATEXT + r'+' + CFWS + r'?' # see 3.2.4
74 | DOT_ATOM_TEXT = ATEXT + r'+(?:\.' + ATEXT + r'+)*' # see 3.2.4
75 | DOT_ATOM = CFWS + r'?' + DOT_ATOM_TEXT + CFWS + r'?' # see 3.2.4
76 | QTEXT = r'[' + NO_WS_CTL + \
77 | r'\x21\x23-\x5b\x5d-\x7e]' # see 3.2.5. Quoted strings
78 | QCONTENT = r'(?:' + QTEXT + r'|' + \
79 | QUOTED_PAIR + r')' # see 3.2.5
80 | QUOTED_STRING = CFWS + r'?' + r'"(?:' + FWS + \
81 | r'?' + QCONTENT + r')*' + FWS + \
82 | r'?' + r'"' + CFWS + r'?'
83 | LOCAL_PART = r'(?:' + DOT_ATOM + r'|' + \
84 | QUOTED_STRING + \
85 | r')' # see 3.4.1. Addr-spec specification
86 | DTEXT = r'[' + NO_WS_CTL + r'\x21-\x5a\x5e-\x7e]' # see 3.4.1
87 | DCONTENT = r'(?:' + DTEXT + r'|' + \
88 | QUOTED_PAIR + r')' # see 3.4.1
89 | DOMAIN_LITERAL = CFWS + r'?' + r'\[' + \
90 | r'(?:' + FWS + r'?' + DCONTENT + \
91 | r')*' + FWS + r'?\]' + CFWS + r'?' # see 3.4.1
92 | DOMAIN = r'(?:' + DOT_ATOM + r'|' + \
93 | DOMAIN_LITERAL + r')' # see 3.4.1
94 | ADDR_SPEC = LOCAL_PART + r'@' + DOMAIN # see 3.4.1
95 |
96 | # A valid address will match exactly the 3.4.1 addr-spec.
97 | VALID_ADDRESS_REGEXP = '^' + ADDR_SPEC + '$'
98 |
99 |
100 | def validate_email_with_regex(email_address):
101 | """
102 | Note that this will only filter out syntax mistakes in emailaddresses.
103 | If a human would think it is probably a valid email, it will most likely pass.
104 | However, it could still very well be that the actual emailaddress has simply
105 | not be claimed by anyone (so then this function fails to devalidate).
106 | """
107 | if not re.match(VALID_ADDRESS_REGEXP, email_address):
108 | emsg = 'Emailaddress "{}" is not valid according to RFC 2822 standards'.format(
109 | email_address)
110 | raise YagInvalidEmailAddress(emsg)
111 | # apart from the standard, I personally do not trust email addresses without dot.
112 | if "." not in email_address and "localhost" not in email_address.lower():
113 | raise YagInvalidEmailAddress("Missing dot in emailaddress")
114 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | Usage
2 | =====
3 | This document aims to show how to use ``yagmail`` in your programs.
4 | Most of what is shown here is also available to see in the
5 | `README `_, some content may be
6 | duplicated for completeness.
7 |
8 |
9 | Start a Connection
10 | ------------------
11 | As mentioned in :ref:`configuring_credentials`, there
12 | are three ways to initialize a connection by instantiating
13 | :class:`yagmail.SMTP`:
14 |
15 | 1. **With Username and Password**:
16 | e.g. ``yagmail.SMTP('mygmailusername', 'mygmailpassword')``
17 | This method is not recommended, since you would be storing
18 | the full credentials to your account in your script in plain text.
19 | A better alternative is using ``keyring``, as described in the
20 | following section:
21 |
22 | 2. **With Username and keyring**:
23 | After registering a ``keyring`` entry for ``yagmail``, you can
24 | instantiate the client by simply passing your username, e.g.
25 | ``yagmail.SMTP('mygmailusername')``.
26 |
27 | 3. **With keyring and .yagmail**:
28 | As explained in the `Setup` documentation, you can also
29 | omit the username if you have a ``.yagmail`` file in your
30 | home folder, containing just your GMail username. This way,
31 | you can initialize :class:`yagmail.SMTP` without any arguments.
32 |
33 | 4. **With OAuth2**:
34 | This is probably the safest method of authentication, as you
35 | can revoke the rights of tokens. To initialize with OAuth2
36 | credentials (after obtaining these as shown in `Setup`),
37 | simply pass an ``oauth2_file`` to :class:`yagmail.SMTP`,
38 | for example ``yagmail.SMTP('user@gmail.com', oauth2_file='~/oauth2_creds.json')``.
39 |
40 |
41 | Closing and reusing the Connection
42 | ----------------------------------
43 | By default, :class:`yagmail.SMTP` will clean up after itself
44 | **in CPython**. This is an implementation detail of CPython and as such
45 | may not work in other implementations such as PyPy (reported in
46 | `issue #39 `_). In those
47 | cases, you can use :class:`yagmail.SMTP` with ``with`` instead.
48 |
49 | Alternatively, you can close and re-use the connection with
50 | :meth:`yagmail.SMTP.close` and :meth:`yagmail.SMTP.login` (or
51 | :meth:`yagmail.SMTP.oauth2_file` if you are using OAuth2).
52 |
53 |
54 | Sending E-Mails
55 | ---------------
56 | :meth:`yagmail.SMTP.send` is a fairly versatile method that allows
57 | you to adjust more or less anything about your Mail.
58 | First of all, all parameters for :meth:`yagmail.SMTP.send` are optional.
59 | If you omit the recipient (specified with ``to``), you will send an
60 | E-Mail to yourself.
61 |
62 | Since the use of the (keyword) arguments of :meth:`yagmail.SMTP.send`
63 | are fairly obvious, they will simply be listed here:
64 |
65 | - ``to``
66 | - ``subject``
67 | - ``contents``
68 | - ``attachments``
69 | - ``cc``
70 | - ``bcc``
71 | - ``preview_only``
72 | - ``headers``
73 |
74 | Some of these - namely ``to`` and ``contents`` - have some magic
75 | associated with them which will be outlined in the following sections.
76 |
77 |
78 | E-Mail recipients
79 | -----------------
80 | You can send an E-Mail to a single user by simply passing
81 | a string with either a GMail username (``@gmail.com`` will be appended
82 | automatically), or with a full E-Mail address:
83 |
84 | .. code-block:: python
85 |
86 | yag.send(to='mike@gmail.com', contents="Hello, Mike!")
87 |
88 | Alternatively, you can send E-Mails to a group of people by either passing
89 | a list or a tuple of E-Mail addresses as ``to``:
90 |
91 | .. code-block:: python
92 |
93 | yag.send(to=['to@someone.com', 'for@someone.com'], contents="Hello there!")
94 |
95 | These E-Mail addresses were passed without any aliases.
96 | If you wish to use aliases for the E-Mail addresses, provide a
97 | dictionary mapped in the form ``{address: alias}``, for example:
98 |
99 | .. code-block:: python
100 |
101 | recipients = {
102 | 'aliased@mike.com': 'Mike',
103 | 'aliased@fred.com': 'Fred'
104 | }
105 | yag.send(to=recipients, contents="Hello, Mike and Fred!")
106 |
107 |
108 | Magical ``contents``
109 | --------------------
110 | The ``contents`` argument of :meth:`yagmail.SMTP.send` will be smartly guessed.
111 | You can pass it a string with your contents or a list of elements which are either:
112 |
113 | - If it is a **dictionary**, then it will be assumed that the key is the content and the value is an alias (currently, this only applies to images). For example:
114 |
115 |
116 | .. code-block:: python
117 |
118 | contents = [
119 | "Hello Mike! Here is a picture I took last week:",
120 | {'path/to/my/image.png': 'PictureForMike'}
121 | ]
122 |
123 | - If it is a **string**, then it will first check whether the content of the string can be **read as a file** locally, for example ``'path/to/my/image.png'``. These files require an extension for their content type to be inferred.
124 |
125 | - If it could not be read locally, then it checks whether the string is valid HTML, such as ``This is a big title!
``.
126 |
127 | - If it was not valid HTML either, then it must be text, such as ``"Hello, Mike!"``.
128 |
129 | If you want to **ensure that a string is treated as text** and should not be checked
130 | for any other content as described above, you can use :class:`yagmail.raw`, a subclass
131 | of :class:`str`.
132 |
133 | If you intend to **inline an image instead of attaching it**, you can use
134 | :class:`yagmail.inline`.
135 |
136 |
137 | Using yagmail from the command line
138 | -----------------------------------
139 | ``yagmail`` includes a command-line application, simply called with ``yagmail``
140 | after you installed it. To view a full reference on how to use this, run
141 | ``yagmail --help``.
142 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # yagmail documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Sep 19 19:40:28 2017.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | import os
20 | import sys
21 | sys.path.insert(0, os.path.abspath('..'))
22 |
23 |
24 | # -- General configuration ------------------------------------------------
25 |
26 | # If your documentation needs a minimal Sphinx version, state it here.
27 | #
28 | # needs_sphinx = '1.0'
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = ['sphinx.ext.autodoc',
34 | 'sphinx.ext.intersphinx',
35 | 'sphinx.ext.viewcode']
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = ['_templates']
39 |
40 | # The suffix(es) of source filenames.
41 | # You can specify multiple suffix as a list of string:
42 | #
43 | # source_suffix = ['.rst', '.md']
44 | source_suffix = '.rst'
45 |
46 | # The master toctree document.
47 | master_doc = 'index'
48 |
49 | # General information about the project.
50 | project = u'yagmail'
51 | copyright = u'2017, kootenpv'
52 | author = u'kootenpv'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | version = u'0.10.189'
60 | # The full version, including alpha/beta/rc tags.
61 | release = u'0.10.189'
62 |
63 | # The language for content autogenerated by Sphinx. Refer to documentation
64 | # for a list of supported languages.
65 | #
66 | # This is also used if you do content translation via gettext catalogs.
67 | # Usually you set "language" from the command line for these cases.
68 | language = None
69 |
70 | # List of patterns, relative to source directory, that match files and
71 | # directories to ignore when looking for source files.
72 | # This patterns also effect to html_static_path and html_extra_path
73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
74 |
75 | # The name of the Pygments (syntax highlighting) style to use.
76 | pygments_style = 'sphinx'
77 |
78 | # If true, `todo` and `todoList` produce output, else they produce nothing.
79 | todo_include_todos = False
80 |
81 |
82 | # -- Options for HTML output ----------------------------------------------
83 |
84 | # The theme to use for HTML and HTML Help pages. See the documentation for
85 | # a list of builtin themes.
86 | #
87 | html_theme = 'alabaster'
88 |
89 | # Theme options are theme-specific and customize the look and feel of a theme
90 | # further. For a list of options available for each theme, see the
91 | # documentation.
92 | #
93 | html_theme_options = {
94 | 'logo': "icon.png",
95 | 'logo_name': True,
96 | 'description': ("yagmail makes sending emails very "
97 | "easy by doing all the magic for you"),
98 |
99 | 'github_user': "kootenpv",
100 | 'github_repo': "yagmail",
101 | 'github_button': True,
102 | 'github_type': 'star'
103 | }
104 |
105 | # Add any paths that contain custom static files (such as style sheets) here,
106 | # relative to this directory. They are copied after the builtin static files,
107 | # so a file named "default.css" will overwrite the builtin "default.css".
108 | html_static_path = ['_static']
109 |
110 | # Custom sidebar templates, must be a dictionary that maps document names
111 | # to template names.
112 | #
113 | # This is required for the alabaster theme
114 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
115 | html_sidebars = {
116 | '**': [
117 | 'about.html',
118 | 'navigation.html',
119 | 'relations.html', # needs 'show_related': True theme option to display
120 | 'searchbox.html',
121 | 'donate.html',
122 | ]
123 | }
124 |
125 |
126 | # -- Options for HTMLHelp output ------------------------------------------
127 |
128 | # Output file base name for HTML help builder.
129 | htmlhelp_basename = 'yagmaildoc'
130 |
131 |
132 | # -- Options for LaTeX output ---------------------------------------------
133 |
134 | latex_elements = {
135 | # The paper size ('letterpaper' or 'a4paper').
136 | #
137 | # 'papersize': 'letterpaper',
138 |
139 | # The font size ('10pt', '11pt' or '12pt').
140 | #
141 | # 'pointsize': '10pt',
142 |
143 | # Additional stuff for the LaTeX preamble.
144 | #
145 | # 'preamble': '',
146 |
147 | # Latex figure (float) alignment
148 | #
149 | # 'figure_align': 'htbp',
150 | }
151 |
152 | # Grouping the document tree into LaTeX files. List of tuples
153 | # (source start file, target name, title,
154 | # author, documentclass [howto, manual, or own class]).
155 | latex_documents = [
156 | (master_doc, 'yagmail.tex', u'yagmail Documentation',
157 | u'kootenpv', 'manual'),
158 | ]
159 |
160 |
161 | # -- Options for manual page output ---------------------------------------
162 |
163 | # One entry per manual page. List of tuples
164 | # (source start file, name, description, authors, manual section).
165 | man_pages = [
166 | (master_doc, 'yagmail', u'yagmail Documentation',
167 | [author], 1)
168 | ]
169 |
170 |
171 | # -- Options for Texinfo output -------------------------------------------
172 |
173 | # Grouping the document tree into Texinfo files. List of tuples
174 | # (source start file, target name, title, author,
175 | # dir menu entry, description, category)
176 | texinfo_documents = [
177 | (master_doc, 'yagmail', u'yagmail Documentation',
178 | author, 'yagmail', 'One line description of project.',
179 | 'Miscellaneous'),
180 | ]
181 |
182 |
183 | # Example configuration for intersphinx: refer to the Python standard library.
184 | intersphinx_mapping = {
185 | "python": ('https://docs.python.org/3', None)
186 | }
187 |
--------------------------------------------------------------------------------
/yagmail/message.py:
--------------------------------------------------------------------------------
1 | import os
2 | from yagmail.compat import text_type
3 | from yagmail.utils import raw, inline
4 | from yagmail.headers import add_subject
5 | from yagmail.headers import add_recipients_headers
6 |
7 | import email.encoders
8 | from email.mime.base import MIMEBase
9 | from email.mime.multipart import MIMEMultipart
10 | from email.mime.text import MIMEText
11 | from email.utils import formatdate
12 | import mimetypes
13 |
14 |
15 | def prepare_message(user, useralias, addresses, subject, contents, attachments, headers, encoding):
16 | # check if closed!!!!!! XXX
17 | """ Prepare a MIME message """
18 | if isinstance(contents, text_type):
19 | contents = [contents]
20 | if isinstance(attachments, text_type):
21 | attachments = [attachments]
22 |
23 | # merge contents and attachments for now.
24 | if attachments is not None:
25 | for a in attachments:
26 | if not os.path.isfile(a):
27 | raise TypeError("'{0}' is not a valid filepath".format(a))
28 | contents = attachments if contents is None else contents + attachments
29 |
30 | has_included_images, content_objects = prepare_contents(contents, encoding)
31 | msg = MIMEMultipart()
32 | if headers is not None:
33 | # Strangely, msg does not have an update method, so then manually.
34 | for k, v in headers.items():
35 | msg[k] = v
36 | if headers is None or "Date" not in headers:
37 | msg["Date"] = formatdate()
38 |
39 | msg_alternative = MIMEMultipart("alternative")
40 | msg_related = MIMEMultipart("related")
41 | msg_related.attach("-- HTML goes here --")
42 | msg.attach(msg_alternative)
43 | add_subject(msg, subject)
44 | add_recipients_headers(user, useralias, msg, addresses)
45 | htmlstr = ""
46 | altstr = []
47 | if has_included_images:
48 | msg.preamble = "This message is best displayed using a MIME capable email reader."
49 |
50 | if contents is not None:
51 | for content_object, content_string in zip(content_objects, contents):
52 | if content_object["main_type"] == "image":
53 | # all image objects need base64 encoding, so do it now
54 | email.encoders.encode_base64(content_object["mime_object"])
55 | # aliased image {'path' : 'alias'}
56 | if isinstance(content_string, dict) and len(content_string) == 1:
57 | for key in content_string:
58 | hashed_ref = str(abs(hash(key)))
59 | alias = content_string[key]
60 | # pylint: disable=undefined-loop-variable
61 | content_string = key
62 | else:
63 | alias = os.path.basename(str(content_string))
64 | hashed_ref = str(abs(hash(alias)))
65 |
66 | # TODO: I should probably remove inline now that there is "attachments"
67 | # if string is `inline`, inline, else, attach
68 | # pylint: disable=unidiomatic-typecheck
69 | if type(content_string) == inline:
70 | htmlstr += '
'.format(hashed_ref, alias)
71 | content_object["mime_object"].add_header(
72 | "Content-ID", "<{0}>".format(hashed_ref)
73 | )
74 | altstr.append("-- img {0} should be here -- ".format(alias))
75 | # inline images should be in related MIME block
76 | msg_related.attach(content_object["mime_object"])
77 | else:
78 | # non-inline images get attached like any other attachment
79 | msg.attach(content_object["mime_object"])
80 |
81 | else:
82 | if content_object["encoding"] == "base64":
83 | email.encoders.encode_base64(content_object["mime_object"])
84 | msg.attach(content_object["mime_object"])
85 | elif content_object["sub_type"] not in ["html", "plain"]:
86 | msg.attach(content_object["mime_object"])
87 | else:
88 | content_string = content_string.replace("\n", "
")
89 | try:
90 | htmlstr += "{0}
".format(content_string)
91 | except UnicodeEncodeError:
92 | htmlstr += u"{0}
".format(content_string)
93 | altstr.append(content_string)
94 |
95 | msg_related.get_payload()[0] = MIMEText(htmlstr, "html", _charset=encoding)
96 | msg_alternative.attach(MIMEText("\n".join(altstr), _charset=encoding))
97 | msg_alternative.attach(msg_related)
98 | return msg
99 |
100 |
101 | def prepare_contents(contents, encoding):
102 | mime_objects = []
103 | has_included_images = False
104 | if contents is not None:
105 | for content in contents:
106 | content_object = get_mime_object(content, encoding)
107 | if content_object["main_type"] == "image":
108 | has_included_images = True
109 | mime_objects.append(content_object)
110 | return has_included_images, mime_objects
111 |
112 |
113 | def get_mime_object(content_string, encoding):
114 | content_object = {"mime_object": None, "encoding": None, "main_type": None, "sub_type": None}
115 |
116 | if isinstance(content_string, dict):
117 | for x in content_string:
118 | content_string, content_name = x, content_string[x]
119 | else:
120 | try:
121 | content_name = os.path.basename(str(content_string))
122 | except UnicodeEncodeError:
123 | content_name = os.path.basename(content_string)
124 | # pylint: disable=unidiomatic-typecheck
125 | is_raw = type(content_string) == raw
126 | if not is_raw and os.path.isfile(content_string):
127 | with open(content_string, "rb") as f:
128 | content_object["encoding"] = "base64"
129 | content = f.read()
130 | else:
131 | content_object["main_type"] = "text"
132 |
133 | if is_raw:
134 | content_object["mime_object"] = MIMEText(content_string, _charset=encoding)
135 | else:
136 | content_object["mime_object"] = MIMEText(content_string, "html", _charset=encoding)
137 | content_object["sub_type"] = "html"
138 |
139 | if content_object["sub_type"] is None:
140 | content_object["sub_type"] = "plain"
141 | return content_object
142 |
143 | if content_object["main_type"] is None:
144 | content_type, _ = mimetypes.guess_type(content_string)
145 |
146 | if content_type is not None:
147 | content_object["main_type"], content_object["sub_type"] = content_type.split("/")
148 |
149 | if content_object["main_type"] is None or content_object["encoding"] is not None:
150 | if content_object["encoding"] != "base64":
151 | content_object["main_type"] = "application"
152 | content_object["sub_type"] = "octet-stream"
153 |
154 | mime_object = MIMEBase(
155 | content_object["main_type"], content_object["sub_type"], name=content_name
156 | )
157 | mime_object.set_payload(content)
158 | content_object["mime_object"] = mime_object
159 | return content_object
160 |
--------------------------------------------------------------------------------
/yagmail/sender.py:
--------------------------------------------------------------------------------
1 | # when there is a bcc a different message has to be sent to the bcc
2 | # person, to show that they are bcc'ed
3 |
4 | import time
5 | import logging
6 | import smtplib
7 |
8 | from yagmail.log import get_logger
9 | from yagmail.utils import find_user_home_path
10 | from yagmail.oauth2 import get_oauth2_info, get_oauth_string
11 | from yagmail.headers import resolve_addresses
12 | from yagmail.validate import validate_email_with_regex
13 | from yagmail.password import handle_password
14 | from yagmail.message import prepare_message
15 | from yagmail.headers import make_addr_alias_user
16 |
17 |
18 | class SMTPBase:
19 | """ :class:`yagmail.SMTP` is a magic wrapper around
20 | ``smtplib``'s SMTP connection, and allows messages to be sent."""
21 |
22 | def __init__(
23 | self,
24 | user=None,
25 | password=None,
26 | host="smtp.gmail.com",
27 | port=None,
28 | smtp_starttls=None,
29 | smtp_ssl=True,
30 | smtp_set_debuglevel=0,
31 | smtp_skip_login=False,
32 | encoding="utf-8",
33 | oauth2_file=None,
34 | soft_email_validation=True,
35 | **kwargs
36 | ):
37 | self.log = get_logger()
38 | self.set_logging()
39 | self.soft_email_validation = soft_email_validation
40 | if oauth2_file is not None:
41 | oauth2_info = get_oauth2_info(oauth2_file)
42 | if user is None:
43 | user = oauth2_info["email_address"]
44 | if smtp_skip_login and user is None:
45 | user = ""
46 | elif user is None:
47 | user = find_user_home_path()
48 | self.user, self.useralias = make_addr_alias_user(user)
49 | if soft_email_validation:
50 | validate_email_with_regex(self.user)
51 | self.is_closed = None
52 | self.host = host
53 | self.port = str(port) if port is not None else "465" if smtp_ssl else "587"
54 | self.smtp_starttls = smtp_starttls
55 | self.ssl = smtp_ssl
56 | self.smtp_skip_login = smtp_skip_login
57 | self.debuglevel = smtp_set_debuglevel
58 | self.encoding = encoding
59 | self.kwargs = kwargs
60 | self.cache = {}
61 | self.unsent = []
62 | self.num_mail_sent = 0
63 | self.oauth2_file = oauth2_file
64 | self.credentials = password if oauth2_file is None else oauth2_info
65 |
66 | def __enter__(self):
67 | return self
68 |
69 | def __exit__(self, exc_type, exc_val, exc_tb):
70 | if not self.is_closed:
71 | self.close()
72 | return False
73 |
74 | @property
75 | def connection(self):
76 | return smtplib.SMTP_SSL if self.ssl else smtplib.SMTP
77 |
78 | @property
79 | def starttls(self):
80 | if self.smtp_starttls is None:
81 | return False if self.ssl else True
82 | return self.smtp_starttls
83 |
84 | def set_logging(self, log_level=logging.ERROR, file_path_name=None):
85 | """
86 | This function allows to change the logging backend, either output or file as backend
87 | It also allows to set the logging level (whether to display only critical/error/info/debug.
88 | for example::
89 |
90 | yag = yagmail.SMTP()
91 | yag.set_logging(yagmail.logging.DEBUG) # to see everything
92 |
93 | and::
94 |
95 | yagmail.set_logging(yagmail.logging.DEBUG, 'somelocalfile.log')
96 |
97 | lastly, a log_level of :py:class:`None` will make sure there is no I/O.
98 | """
99 | self.log = get_logger(log_level, file_path_name)
100 |
101 | def prepare_send(
102 | self,
103 | to=None,
104 | subject=None,
105 | contents=None,
106 | attachments=None,
107 | cc=None,
108 | bcc=None,
109 | headers=None,
110 | ):
111 | addresses = resolve_addresses(self.user, self.useralias, to, cc, bcc)
112 |
113 | if self.soft_email_validation:
114 | for email_addr in addresses["recipients"]:
115 | validate_email_with_regex(email_addr)
116 |
117 | msg = prepare_message(
118 | self.user,
119 | self.useralias,
120 | addresses,
121 | subject,
122 | contents,
123 | attachments,
124 | headers,
125 | self.encoding,
126 | )
127 |
128 | recipients = addresses["recipients"]
129 | msg_string = msg.as_string()
130 | return recipients, msg_string
131 |
132 | def send(
133 | self,
134 | to=None,
135 | subject=None,
136 | contents=None,
137 | attachments=None,
138 | cc=None,
139 | bcc=None,
140 | preview_only=False,
141 | headers=None,
142 | ):
143 | """ Use this to send an email with gmail"""
144 | self.login()
145 | recipients, msg_string = self.prepare_send(
146 | to, subject, contents, attachments, cc, bcc, headers
147 | )
148 | if preview_only:
149 | return (recipients, msg_string)
150 | return self._attempt_send(recipients, msg_string)
151 |
152 | def _attempt_send(self, recipients, msg_string):
153 | attempts = 0
154 | while attempts < 3:
155 | try:
156 | result = self.smtp.sendmail(self.user, recipients, msg_string)
157 | self.log.info("Message sent to %s", recipients)
158 | self.num_mail_sent += 1
159 | return result
160 | except smtplib.SMTPServerDisconnected as e:
161 | self.log.error(e)
162 | attempts += 1
163 | time.sleep(attempts * 3)
164 | self.unsent.append((recipients, msg_string))
165 | return False
166 |
167 | def send_unsent(self):
168 | """
169 | Emails that were not being able to send will be stored in :attr:`self.unsent`.
170 | Use this function to attempt to send these again
171 | """
172 | for i in range(len(self.unsent)):
173 | recipients, msg_string = self.unsent.pop(i)
174 | self._attempt_send(recipients, msg_string)
175 |
176 | def close(self):
177 | """ Close the connection to the SMTP server """
178 | self.is_closed = True
179 | try:
180 | self.smtp.quit()
181 | except (TypeError, AttributeError, smtplib.SMTPServerDisconnected):
182 | pass
183 |
184 | def _login(self, password):
185 | """
186 | Login to the SMTP server using password. `login` only needs to be manually run when the
187 | connection to the SMTP server was closed by the user.
188 | """
189 | self.smtp = self.connection(self.host, self.port, **self.kwargs)
190 | self.smtp.set_debuglevel(self.debuglevel)
191 | if self.starttls:
192 | self.smtp.ehlo()
193 | if self.starttls is True:
194 | self.smtp.starttls()
195 | else:
196 | self.smtp.starttls(**self.starttls)
197 | self.smtp.ehlo()
198 | self.is_closed = False
199 | if not self.smtp_skip_login:
200 | password = self.handle_password(self.user, password)
201 | self.smtp.login(self.user, password)
202 | self.log.info("Connected to SMTP @ %s:%s as %s", self.host, self.port, self.user)
203 |
204 | @staticmethod
205 | def handle_password(user, password):
206 | return handle_password(user, password)
207 |
208 | @staticmethod
209 | def get_oauth_string(user, oauth2_info):
210 | return get_oauth_string(user, oauth2_info)
211 |
212 | def _login_oauth2(self, oauth2_info):
213 | if "email_address" in oauth2_info:
214 | oauth2_info.pop("email_address")
215 | self.smtp = self.connection(self.host, self.port, **self.kwargs)
216 | try:
217 | self.smtp.set_debuglevel(self.debuglevel)
218 | except AttributeError:
219 | pass
220 | auth_string = self.get_oauth_string(self.user, oauth2_info)
221 | self.smtp.ehlo(oauth2_info["google_client_id"])
222 | if self.starttls is True:
223 | self.smtp.starttls()
224 | self.smtp.docmd("AUTH", "XOAUTH2 " + auth_string)
225 |
226 | def feedback(self, message="Awesome features! You made my day! How can I contribute?"):
227 | """ Most important function. Please send me feedback :-) """
228 | self.send("kootenpv@gmail.com", "Yagmail feedback", message)
229 |
230 | def __del__(self):
231 | try:
232 | if not self.is_closed:
233 | self.close()
234 | except AttributeError:
235 | pass
236 |
237 |
238 | class SMTP(SMTPBase):
239 | def login(self):
240 | if self.oauth2_file is not None:
241 | self._login_oauth2(self.credentials)
242 | else:
243 | self._login(self.credentials)
244 |
245 |
246 | class SMTP_SSL(SMTP):
247 | def __init__(self, *args, **kwargs):
248 | import warnings
249 |
250 | warnings.warn(
251 | "It is now possible to simply use 'SMTP' with smtp_ssl=True",
252 | category=DeprecationWarning,
253 | )
254 | kwargs["smtp_ssl"] = True
255 | super(SMTP_SSL, self).__init__(*args, **kwargs)
256 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # yagmail -- Yet Another GMAIL/SMTP client
7 |
8 | [](https://gitter.im/kootenpv/yagmail?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 | [](https://pypi.python.org/pypi/yagmail/)
10 | [](https://pypi.python.org/pypi/yagmail/)
11 |
12 | The goal here is to make it as simple and painless as possible to send emails.
13 |
14 | In the end, your code will look something like this:
15 |
16 | ```python
17 | import yagmail
18 | yag = yagmail.SMTP()
19 | contents = ['This is the body, and here is just text http://somedomain/image.png',
20 | 'You can find an audio file attached.', '/local/path/song.mp3']
21 | yag.send('to@someone.com', 'subject', contents)
22 | ```
23 |
24 | Or a simple one-liner:
25 | ```python
26 | yagmail.SMTP('mygmailusername').send('to@someone.com', 'subject', 'This is the body')
27 | ```
28 |
29 | Note that it will read the password securely from your keyring (read below). If you don't want this, you can also initialize with:
30 |
31 | ```python
32 | yag = yagmail.SMTP('mygmailusername', 'mygmailpassword')
33 | ```
34 |
35 | but honestly, do you want to have your password written in your script?
36 |
37 | Similarly, I make use of having my username in a file named `.yagmail` in my home folder.
38 |
39 | ### Table of Contents
40 |
41 | |Section|Explanation|
42 | |---------------------------------------------------------------|---------------------------------------------------------------------|
43 | |[Install](#install) | Find the instructions on how to install yagmail here |
44 | |[Username and password](#username-and-password) | No more need to fill in username and password in scripts |
45 | |[Start a connection](#start-a-connection) | Get started |
46 | |[Usability](#usability) | Shows some usage patterns for sending |
47 | |[Recipients](#recipients) | How to send to multiple people, give an alias or send to self |
48 | |[Magical contents](#magical-contents) | Really easy to send text, html, images and attachments |
49 | |[Feedback](#feedback) | How to send me feedback |
50 | |[Roadmap (and priorities)](#roadmap-and-priorities) | Yup |
51 | |[Errors](#errors) | List of common errors for people dealing with sending emails |
52 |
53 |
54 | ### Install
55 |
56 | For Python 2.x and Python 3.x respectively:
57 |
58 | ```python
59 | pip install yagmail[all]
60 | pip3 install yagmail[all]
61 |
62 | ```
63 |
64 | If you get problems installing keyring, try installing without, i.e. `pip install yagmail`.
65 |
66 | As a side note, `yagmail` can now also be used to send emails from the command line.
67 |
68 | ### Username and password
69 |
70 | [keyring quoted](https://pypi.python.org/pypi/keyring#what-is-python-keyring-lib):
71 | > The Python `keyring` lib provides a easy way to access the system keyring service from python. It can be used in any application that needs safe password storage.
72 |
73 | You know you want it. Set it up by opening a Python interpreter and running:
74 |
75 | ```python
76 | import yagmail
77 | yagmail.register('mygmailusername', 'mygmailpassword')
78 | ```
79 |
80 | In fact, it is just a wrapper for `keyring.set_password('yagmail', 'mygmailusername', 'mygmailpassword')`.
81 |
82 | When no password is given and the user is not found in the keyring, `getpass.getpass()` is used to prompt the user for a password. Upon entering this once, it can be stored in the keyring and never asked again.
83 |
84 | Another convenience can be to save a .yagmail file in your home folder, containing just the email username. You can then omit everything, and simply use `yagmail.SMTP()` to connect. Of course, this wouldn't work with more accounts, but it might be a nice default. Upon request I'll consider adding more details to this .yagmail file (host, port and other settings).
85 |
86 | ### Start a connection
87 |
88 | ```python
89 | yag = yagmail.SMTP('mygmailusername')
90 | ```
91 |
92 | Note that this connection is reusable, closable and when it leaves scope it will **clean up after itself in CPython**.
93 |
94 | As [tilgovi](https://github.com/tilgovi) points out in [#39](https://github.com/kootenpv/yagmail/issues/39), SMTP does not automatically close in **PyPy**. The context manager `with` should be used in that case.
95 |
96 |
97 | ### Usability
98 |
99 | Defining some variables:
100 |
101 | ```python
102 | to = 'santa@someone.com'
103 | to2 = 'easterbunny@someone.com
104 | to3 = 'sky@pip-package.com'
105 | subject = 'This is obviously the subject'
106 | body = 'This is obviously the body'
107 | html = 'Click me!'
108 | img = '/local/file/bunny.png'
109 | ```
110 |
111 | All variables are optional, and know that not even `to` is required (you'll send an email to yourself):
112 |
113 | ```python
114 | yag.send(to = to, subject = subject, contents = body)
115 | yag.send(to = to, subject = subject, contents = [body, html, img])
116 | yag.send(contents = [body, img])
117 | ```
118 |
119 | Furthermore, if you do not want to be explicit, you can do the following:
120 |
121 | ```python
122 | yag.send(to, subject, [body, img])
123 | ```
124 |
125 | ### Recipients
126 |
127 | It is also possible to send to a group of people by providing a list of email strings rather than a single string:
128 |
129 | ```python
130 | yag.send(to = to)
131 | yag.send(to = [to, to2]) # List or tuples for emailadresses *without* aliases
132 | yag.send(to = {to : 'Alias1'}) # Dictionary for emailaddress *with* aliases
133 | yag.send(to = {to : 'Alias1', to2 : 'Alias2'}
134 | ```
135 |
136 | Giving no `to` argument will send an email to yourself. In that sense, `yagmail.SMTP().send()` can already send an email.
137 | Be aware that if no explicit `to = ...` is used, the first argument will be used to send to. Can be avoided like:
138 |
139 | ```python
140 | yag.send(subject = 'to self', contents = 'hi!')
141 | ```
142 |
143 | Note that by default all email addresses are conservatively validated using `soft_email_validation==True` (default).
144 |
145 | ### Oauth2
146 |
147 | It is even safer to use Oauth2 for authentication, as you can revoke the rights of tokens.
148 |
149 | [This](http://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html) is one of the best sources, upon which the oauth2 code is heavily based.
150 |
151 | The code:
152 |
153 | ```python
154 | yag = SMTP("user@gmail.com", oauth2_file="~/oauth2_creds.json")
155 | yag.send(subject="Great!")
156 | ```
157 |
158 | It will prompt for a `google_client_id` and a `google_client_secret`, when the file cannot be found. These variables can be obtained following [the previous link](http://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html).
159 |
160 | After you provide them, a link will be shown in the terminal that you should followed to obtain a `google_refresh_token`. Paste this again, and you're set up!
161 |
162 | Note that people who obtain the file can send emails, but nothing else. As soon as you notice, you can simply disable the token.
163 |
164 | ### Magical `contents`
165 |
166 | The `contents` argument will be smartly guessed. It can be passed a string (which will be turned into a list); or a list. For each object in the list:
167 |
168 | - If it is a dictionary it will assume the key is the content and the value is an alias (only for images currently!)
169 | e.g. {'/path/to/image.png' : 'MyPicture'}
170 | - It will try to see if the content (string) can be read as a file locally,
171 | e.g. '/path/to/image.png'
172 | - if impossible, it will check if the string is valid html
173 | e.g. `This is a big title
`
174 | - if not, it must be text.
175 | e.g. 'Hi Dorika!'
176 |
177 | Note that local files can be html (inline); everything else will be attached.
178 |
179 | Local files require to have an extension for their content type to be inferred.
180 |
181 | As of version 0.4.94, `raw` and `inline` have been added.
182 |
183 | - `raw` ensures a string will not receive any "magic" (inlining, html, attaching)
184 | - `inline` will make an image appear in the text.
185 |
186 | ### Feedback
187 |
188 | I'll try to respond to issues within 24 hours at Github.....
189 |
190 | And please send me a line of feedback with `SMTP().feedback('Great job!')` :-)
191 |
192 | ### Roadmap (and priorities)
193 |
194 | - ~~Added possibility of Image~~
195 | - ~~Optional SMTP arguments should go with \**kwargs to my SMTP~~
196 | - ~~CC/BCC (high)~~
197 | - ~~Custom names (high)~~
198 | - ~~Allow send to return a preview rather than to actually send~~
199 | - ~~Just use attachments in "contents", being smart guessed (high, complex)~~
200 | - ~~Attachments (contents) in a list so they actually define the order (medium)~~
201 | - ~~Use lxml to see if it can parse the html (low)~~
202 | - ~~Added tests (high)~~
203 | - ~~Allow caching of content (low)~~
204 | - ~~Extra other types (low)~~ (for example, mp3 also works, let me know if something does not work)
205 | - ~~Probably a naming issue with content type/default type~~
206 | - ~~Choose inline or not somehow (high)~~
207 | - ~~Make lxml module optional magic (high)~~
208 | - ~~Provide automatic fallback for complex content(medium)~~ (should work)
209 | - ~~`yagmail` as a command on CLI upon install~~
210 | - ~~Added `feedback` function on SMTP to be able to send me feedback directly :-)~~
211 | - ~~Added the option to validate emailaddresses...~~
212 | - ~~however, I'm unhappy with the error handling/loggin of wrong emails~~
213 | - ~~Logging count & mail capability (very low)~~
214 | - ~~Add documentation to exception classes (low)~~
215 | - ~~add `raw` and `inline```~~
216 | - ~~oauth2~~
217 | - ~~Travis CI integration ~~
218 | - ~~ Add documentation to all functions (high, halfway) ~~
219 | - Prepare for official 1.0
220 | - Go over documentation again (medium)
221 | - Allow `.yagmail` file to contain more parameters (medium)
222 | - Add option to shrink images (low)
223 |
224 | ### Errors
225 |
226 | - Make sure you have a keyring entry (see section No more password and username), or use getpass to register. I discourage to use username / password in scripts.
227 |
228 | - [`smtplib.SMTPException: SMTP AUTH extension not supported by server`](http://stackoverflow.com/questions/10147455/trying-to-send-email-gmail-as-mail-provider-using-python)
229 |
230 | - [`SMTPAuthenticationError: Application-specific password required`](https://support.google.com/accounts/answer/185833)
231 |
232 | - **YagAddressError**: This means that the address was given in an invalid format. Note that `From` can either be a string, or a dictionary where the key is an `email`, and the value is an `alias` {'sample@gmail.com': 'Sam'}. In the case of 'to', it can either be a string (`email`), a list of emails (email addresses without aliases) or a dictionary where keys are the email addresses and the values indicate the aliases.
233 |
234 | - **YagInvalidEmailAddress**: Note that this will only filter out syntax mistakes in emailaddresses. If a human would think it is probably a valid email, it will most likely pass. However, it could still very well be that the actual emailaddress has simply not be claimed by anyone (so then this function fails to devalidate).
235 |
236 | - Click to enable the email for being used externally https://www.google.com/settings/security/lesssecureapps
237 |
238 | - Make sure you have a working internet connection
239 |
240 | - If you get an `ImportError` try to install with `sudo`, see issue #13
241 |
242 | ### Donate
243 |
244 | If you like `yagmail`, feel free (no pun intended) to donate any amount you'd like :-)
245 |
246 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Y7QCCEPGC6R5E)
247 |
--------------------------------------------------------------------------------