Full Code of lavr/python-emails for AI

master 3a5a55c390de cached
91 files
296.2 KB
75.3k tokens
437 symbols
1 requests
Download .txt
Showing preview only (320K chars total). Download the full file or copy to clipboard to get everything.
Repository: lavr/python-emails
Branch: master
Commit: 3a5a55c390de
Files: 91
Total size: 296.2 KB

Directory structure:
gitextract_9o3m1iyk/

├── .coveragerc
├── .github/
│   └── workflows/
│       ├── python-publish.yml
│       └── tests.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── AUTHORS.rst
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.rst
├── docs/
│   ├── Makefile
│   ├── _static/
│   │   └── .gitkeep
│   ├── advanced.rst
│   ├── api.rst
│   ├── conf.py
│   ├── examples.rst
│   ├── faq.rst
│   ├── howtohelp.rst
│   ├── index.rst
│   ├── install.rst
│   ├── links.rst
│   ├── quickstart.rst
│   ├── requirements.txt
│   └── transformations.rst
├── emails/
│   ├── __init__.py
│   ├── backend/
│   │   ├── __init__.py
│   │   ├── factory.py
│   │   ├── inmemory/
│   │   │   └── __init__.py
│   │   ├── response.py
│   │   └── smtp/
│   │       ├── __init__.py
│   │       ├── aio_backend.py
│   │       ├── aio_client.py
│   │       ├── backend.py
│   │       ├── client.py
│   │       └── exceptions.py
│   ├── django/
│   │   └── __init__.py
│   ├── django_.py
│   ├── exc.py
│   ├── loader/
│   │   ├── __init__.py
│   │   └── helpers.py
│   ├── message.py
│   ├── py.typed
│   ├── signers.py
│   ├── store/
│   │   ├── __init__.py
│   │   ├── file.py
│   │   └── store.py
│   ├── template/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── jinja_template.py
│   │   └── mako_template.py
│   ├── testsuite/
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   ├── django_/
│   │   │   └── test_django_integrations.py
│   │   ├── loader/
│   │   │   ├── data/
│   │   │   │   └── html_import/
│   │   │   │       └── oldornament/
│   │   │   │           └── oldornament/
│   │   │   │               └── index.html
│   │   │   ├── test_helpers.py
│   │   │   ├── test_loaders.py
│   │   │   └── test_rfc822_loader.py
│   │   ├── message/
│   │   │   ├── __init__.py
│   │   │   ├── helpers.py
│   │   │   ├── test_dkim.py
│   │   │   ├── test_lazy_gettext.py
│   │   │   ├── test_message.py
│   │   │   ├── test_send.py
│   │   │   ├── test_send_async.py
│   │   │   ├── test_send_async_e2e.py
│   │   │   └── test_template.py
│   │   ├── smtp/
│   │   │   ├── test_aio_client.py
│   │   │   ├── test_async_smtp_backend.py
│   │   │   ├── test_factory.py
│   │   │   ├── test_smtp_backend.py
│   │   │   └── test_smtp_response.py
│   │   ├── smtp_servers.py
│   │   ├── store/
│   │   │   └── test_store.py
│   │   ├── test_templates.py
│   │   ├── test_utils.py
│   │   └── transformer/
│   │       ├── data/
│   │       │   └── premailer_load/
│   │       │       └── style.css
│   │       ├── test_parser.py
│   │       └── test_transformer.py
│   ├── transformer.py
│   └── utils.py
├── release.sh
├── requirements/
│   ├── base.txt
│   ├── tests-base.txt
│   ├── tests-django.txt
│   └── tests.txt
├── scripts/
│   └── make_rfc822.py
├── setup.cfg
├── setup.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = emails

[report]
omit = 
    emails/testsuite*
    emails/packages*
    emails/compat*


================================================
FILE: .github/workflows/python-publish.yml
================================================
name: Upload Python Package

on:
  release:
    types: [created]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install build tools
        run: pip install build
      - name: Build package
        run: python -m build
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1


================================================
FILE: .github/workflows/tests.yaml
================================================
name: Tests
on:
  push:
    branches:
      - master
      - '*'
    tags:
      - '**'
  pull_request:
    branches:
      - master

jobs:
  tests:
    name: "unit / ${{ matrix.name }}"
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - {name: '3.14', python: '3.14', os: ubuntu-latest, tox: py314}
          - {name: '3.13', python: '3.13', os: ubuntu-latest, tox: py313}
          - {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312}
          - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}
          - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: pip
      - name: update pip
        run: |
          pip install -U wheel
          pip install -U setuptools
          python -m pip install -U pip
      - run: pip install tox
      - name: run tests
        run: tox -e ${{ matrix.tox }} -- -m "not e2e and not django"

  django:
    name: "django / ${{ matrix.django }}"
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - {django: '4.2', tox: django42}
          - {django: '5.2', tox: django52}
          - {django: '6.0', tox: django60}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip
      - name: update pip
        run: |
          pip install -U wheel
          pip install -U setuptools
          python -m pip install -U pip
      - run: pip install tox
      - name: run django tests
        run: tox -e ${{ matrix.tox }}

  docs:
    name: "docs"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip
      - name: install dependencies
        run: |
          pip install -e ".[html]"
          pip install sphinx -r docs/requirements.txt
      - name: build docs
        run: sphinx-build -W -b html docs docs/_build/html
      - name: run doctests
        run: sphinx-build -b doctest docs docs/_build/doctest

  typecheck:
    name: "typecheck"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip
      - name: update pip
        run: |
          pip install -U wheel
          pip install -U setuptools
          python -m pip install -U pip
      - run: pip install tox
      - name: run mypy
        run: tox -e typecheck

  e2e:
    name: "e2e / ${{ matrix.name }}"
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - {name: '3.14', python: '3.14', os: ubuntu-latest, tox: py314}
    services:
      mailpit:
        image: axllent/mailpit
        ports:
          - 1025:1025
          - 8025:8025
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: pip
      - name: update pip
        run: |
          pip install -U wheel
          pip install -U setuptools
          python -m pip install -U pip
      - run: pip install tox
      - name: run e2e tests
        env:
          SMTP_TEST_SUBJECT_SUFFIX: "github-actions sha:${{ github.sha }} run_id:${{ github.run_id }}"
          SMTP_TEST_MAIL_FROM: python-emails-tests@lavr.me
          SMTP_TEST_MAIL_TO: python-emails-tests@lavr.me
          SMTP_TEST_SETS: LOCAL
          SMTP_TEST_LOCAL_HOST: 127.0.0.1
          SMTP_TEST_LOCAL_PORT: 1025
          SMTP_TEST_LOCAL_WITHOUT_TLS: true
        run: tox -e ${{ matrix.tox }} -- -m e2e

  publish_rtd:
    name: "publish read the docs"
    needs:
      - tests
      - django
      - docs
      - typecheck
      - e2e
    if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
    runs-on: ubuntu-latest
    env:
      RTD_API_TOKEN: ${{ secrets.RTD_API_TOKEN }}
      RTD_PROJECT_SLUG: python-emails
    steps:
      - name: Trigger Read the Docs build
        run: |
          set -euo pipefail

          : "${RTD_API_TOKEN:?RTD_API_TOKEN secret is required}"

          api_base="https://app.readthedocs.org/api/v3/projects/${RTD_PROJECT_SLUG}"
          auth_header="Authorization: Token ${RTD_API_TOKEN}"

          get_version_details() {
            local version_slug="$1"
            local response_file="${2:-version.json}"

            curl \
              --silent \
              --show-error \
              --output "${response_file}" \
              --write-out '%{http_code}' \
              --header "${auth_header}" \
              "${api_base}/versions/${version_slug}/"
          }

          wait_for_version_slug() {
            local version_name="$1"

            for attempt in {1..12}; do
              local status_code
              local version_slug
              status_code="$(
                curl \
                  --silent \
                  --show-error \
                  --output versions.json \
                  --write-out '%{http_code}' \
                  --get \
                  --header "${auth_header}" \
                  --data-urlencode "type=tag" \
                  --data-urlencode "verbose_name=${version_name}" \
                  "${api_base}/versions/"
              )"

              if [[ "${status_code}" == "200" ]]; then
                version_slug="$(
                  jq \
                    --raw-output \
                    --arg version_name "${version_name}" \
                    '.results[] | select(.verbose_name == $version_name) | .slug' \
                    versions.json | head -n 1
                )"

                if [[ -n "${version_slug}" && "${version_slug}" != "null" ]]; then
                  printf '%s\n' "${version_slug}"
                  return 0
                fi
              fi

              sleep 5
            done

            echo "Read the Docs version '${version_name}' was not found after sync."
            if [[ -f versions.json ]]; then
              cat versions.json
            fi
            return 1
          }

          trigger_build() {
            local version_slug="$1"

            echo "Triggering Read the Docs build for version slug '${version_slug}'."
            curl \
              --fail-with-body \
              --silent \
              --show-error \
              --request POST \
              --header "${auth_header}" \
              "${api_base}/versions/${version_slug}/builds/"
          }

          if [[ "${GITHUB_REF_TYPE}" == "branch" ]]; then
            trigger_build latest
            exit 0
          fi

          version_name="${GITHUB_REF_NAME}"

          curl \
            --fail-with-body \
            --silent \
            --show-error \
            --request POST \
            --header "${auth_header}" \
            "${api_base}/sync-versions/"

          version_slug="$(wait_for_version_slug "${version_name}")"
          status_code="$(get_version_details "${version_slug}")"

          if [[ "${status_code}" != "200" ]]; then
            echo "Failed to fetch Read the Docs version details for '${version_slug}'."
            cat version.json
            exit 1
          fi

          active="$(jq -r '.active' version.json)"
          hidden="$(jq -r '.hidden' version.json)"
          echo "Read the Docs version '${version_slug}' status: active=${active}, hidden=${hidden}."

          if [[ "${active}" == "true" && "${hidden}" == "false" ]]; then
            trigger_build "${version_slug}"
            exit 0
          fi

          echo "Activating and unhiding Read the Docs version '${version_slug}'."
          curl \
            --fail-with-body \
            --silent \
            --show-error \
            --request PATCH \
            --header "${auth_header}" \
            --header "Content-Type: application/json" \
            --data '{"active": true, "hidden": false}' \
            "${api_base}/versions/${version_slug}/"

          status_code="$(get_version_details "${version_slug}")"

          if [[ "${status_code}" != "200" ]]; then
            echo "Failed to re-fetch Read the Docs version details for '${version_slug}' after PATCH."
            cat version.json
            exit 1
          fi

          active="$(jq -r '.active' version.json)"
          hidden="$(jq -r '.hidden' version.json)"
          echo "Read the Docs version '${version_slug}' updated status: active=${active}, hidden=${hidden}."

          if [[ "${active}" == "true" && "${hidden}" == "false" ]]; then
            trigger_build "${version_slug}"
            exit 0
          fi

          echo "Read the Docs version '${version_slug}' is still not buildable after PATCH."
          cat version.json
          exit 1


================================================
FILE: .gitignore
================================================
local_settings.py
local_*.py
*.py[cod]

# C extensions
*.so

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox
nosetests.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.idea

venv/
.env

docs/plans/
docs/_build/
.claude/*local*
.claude/worktrees/

# CodeQL
.codeql-db
codeql-results.sarif

# ralphex progress logs
.ralphex/progress/


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/asottile/pyupgrade
    rev: v3.21.2
    hooks:
      - id: pyupgrade
        args: ["--py310-plus"]
  - repo: https://github.com/asottile/reorder_python_imports
    rev: v3.16.0
    hooks:
      - id: reorder-python-imports
        name: Reorder Python imports (src, tests)
        files: "^(?!examples/)"
        args: ["--application-directories", "src"]
  - repo: https://github.com/python/black
    rev: 26.3.1
    hooks:
      - id: black
  - repo: https://github.com/pycqa/flake8
    rev: 7.3.0
    hooks:
      - id: flake8
        additional_dependencies:
          - flake8-bugbear
          - flake8-implicit-str-concat
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-byte-order-marker
      - id: trailing-whitespace
      - id: end-of-file-fixer



================================================
FILE: .readthedocs.yaml
================================================
version: 2

build:
  os: ubuntu-24.04
  tools:
    python: "3.12"

sphinx:
  configuration: docs/conf.py

python:
  install:
    - requirements: docs/requirements.txt
    - method: pip
      path: .


================================================
FILE: AUTHORS.rst
================================================
Authors
```````

python-emails is maintained by:

- `@lavr <https://github.com/lavr>`_

With inputs and contributions from (in chronological order):

- `@smihai <https://github.com/smihai>`_
- `@Daviey <https://github.com/Daviey>`_
- `@positiveviking <https://github.com/positiveviking>`_

See `all Github contributors <https://github.com/lavr/python-emails/graphs/contributors>`_



================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 1.0.2

### Added

- Documentation build check in CI (#208)
- ReadTheDocs configuration (`.readthedocs.yaml`)

### Fixed

- Jinja2 is now an optional dependency — install with `pip install emails[jinja2]` (#207, #161)

## 1.0

### Breaking changes

- Require Python 3.10+ (dropped 3.9) (#188)
- HTML transformation dependencies (`cssutils`, `lxml`, `chardet`, `requests`, `premailer`) are now optional — install with `pip install emails[html]` (#190)
- Removed Python 2 compatibility helpers `to_bytes`, `to_native`, `to_unicode` from `emails.utils` (#197)
- Replaced vendored `emails.packages.dkim` with upstream `dkimpy` package — use `import dkim` directly (#196)

### Added

- `reply_to` parameter for Message (#115)
- Content-based MIME type detection via `puremagic` when file extension is missing (#163)
- Data URI support in transformer — `data:` URIs are preserved as-is (#62)
- Type hints for public API (#191)
- mypy in CI (#194)
- Python 3.13 and 3.14 support (#184)
- Django CI jobs with Django 4.2, 5.2, 6.0 (#201)
- CC/BCC importing in MsgLoader (#182)
- RFC 6532 support — non-ASCII characters in email addresses (#138)
- In-memory SMTP backend (#136)
- SMTP integration tests using Mailpit (#186)

### Fixed

- Double stream read in `BaseFile.mime` for file-like attachments (#199)
- `as_bytes` DKIM signing bug (#194)
- SMTP connection is now properly closed on any initialization failure (#180)
- SMTP connection is now properly closed on failed login (#173)
- Incorrect `isinstance` check in `parse_name_and_email_list` (#176)
- Message encoding to bytes in SMTP backend (#152)
- Unique filename generation for attachments
- Regex escape sequence warning (#148)
- Replaced deprecated `cgi` module with `email.message`
- Coverage reports now correctly exclude `emails/testsuite/`

### Maintenance

- Removed vendored dkim package (~1400 lines)
- Removed Python 2 compatibility code and helpers (#188, #197, #198)
- Updated pre-commit hooks to current versions
- Updated GitHub Actions to supported versions
- Removed universal wheel flag (py3-only)
- Cleaned up documentation and project metadata
- Added Python 3.12 to test matrix (#169)

## 0.6 — 2019-07-14

Last release before the changelog was introduced.


================================================
FILE: LICENSE
================================================
Copyright 2013-2015 Sergey Lavrinenko

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

================================================
FILE: MANIFEST.in
================================================
include README.rst LICENSE requirements.txt
include emails/py.typed


================================================
FILE: Makefile
================================================

DOCS_PYTHON = .venv/bin/python
DOCS_SOURCE = docs
DOCS_BUILD = $(DOCS_SOURCE)/_build/html
SPHINXOPTS ?=

.PHONY: clean docs test pypi codeql-db codeql-analyze codeql-clean

clean:
	find . -name '*.pyc'  -exec rm -f {} \;
	find . -name '*.py~'  -exec rm -f {} \;
	find . -name '__pycache__'  -exec rm -rf {} \;
	find . -name '.coverage.*' -exec rm -rf {} \;
	rm -rf build dist emails.egg-info tmp-emails _files $(DOCS_SOURCE)/_build

docs:
	$(DOCS_PYTHON) -m sphinx -b html $(SPHINXOPTS) $(DOCS_SOURCE) $(DOCS_BUILD)
	@echo
	@echo "Build finished. Open $(DOCS_BUILD)/index.html"

test:
	tox

pypi:
	python setup.py sdist bdist_wheel upload

CODEQL_DB = .codeql-db
CODEQL_PYTHON = .venv/bin/python

codeql-db:
	rm -rf $(CODEQL_DB)
	codeql database create $(CODEQL_DB) --language=python --source-root=. \
		--extractor-option=python.python_executable=$(CODEQL_PYTHON)

codeql-analyze: codeql-db
	codeql pack download codeql/python-queries
	codeql database analyze $(CODEQL_DB) codeql/python-queries \
		--format=sarif-latest --output=codeql-results.sarif

codeql-clean:
	rm -rf $(CODEQL_DB) codeql-results.sarif


================================================
FILE: README.rst
================================================
python-emails
=============

.. |pypi| image:: https://img.shields.io/pypi/v/emails.svg
   :target: https://pypi.org/project/emails/
   :alt: PyPI version

.. |python| image:: https://img.shields.io/pypi/pyversions/emails.svg
   :target: https://pypi.org/project/emails/
   :alt: Python versions

.. |tests| image:: https://github.com/lavr/python-emails/workflows/Tests/badge.svg?branch=master
   :target: https://github.com/lavr/python-emails/actions?query=workflow%3ATests
   :alt: Test status

.. |docs| image:: https://readthedocs.org/projects/python-emails/badge/?version=latest
   :target: https://python-emails.readthedocs.io/
   :alt: Documentation status

.. |license| image:: https://img.shields.io/pypi/l/emails.svg
   :target: https://github.com/lavr/python-emails/blob/master/LICENSE
   :alt: License

|pypi| |python| |tests| |docs| |license|

Build, transform, and send emails in Python with a high-level API.

``python-emails`` helps you compose HTML and plain-text messages, attach files,
embed inline images, render templates, apply HTML transformations, sign with
DKIM, and send through SMTP without hand-building MIME trees.


Why python-emails
-----------------

- A concise API over ``email`` and ``smtplib``
- HTML and plain-text messages in one object
- File attachments and inline images
- CSS inlining, image embedding, and HTML cleanup
- Jinja2, Mako, and string template support
- DKIM signing
- Loaders for URLs, HTML files, directories, ZIP archives, and RFC 822 messages
- SMTP sending with SSL/TLS support
- Async sending via ``aiosmtplib``


Quick Example
-------------

.. code-block:: python

    import emails

    message = emails.html(
        subject="Your receipt",
        html="<p>Hello!</p><p>Your payment was received.</p>",
        mail_from=("Billing", "billing@example.com"),
    )
    message.attach(filename="receipt.pdf", data=open("receipt.pdf", "rb"))

    response = message.send(
        to="customer@example.com",
        smtp={
            "host": "smtp.example.com",
            "port": 587,
            "tls": True,
            "user": "billing@example.com",
            "password": "app-password",
        },
    )
    assert response.status_code == 250


Installation
------------

Install the lightweight core:

.. code-block:: bash

    pip install emails

Install HTML transformation features such as CSS inlining, image embedding,
and loading from URLs or files:

.. code-block:: bash

    pip install "emails[html]"

Install Jinja2 template support for the ``JinjaTemplate`` class:

.. code-block:: bash

    pip install "emails[jinja]"

Install async SMTP sending support for ``send_async()``:

.. code-block:: bash

    pip install "emails[async]"


Common Tasks
------------

- Build and send your first message:
  `Quickstart <https://python-emails.readthedocs.io/en/latest/quickstart.html>`_
- Configure installation extras:
  `Install guide <https://python-emails.readthedocs.io/en/latest/install.html>`_
- Inline CSS, embed images, and customize HTML processing:
  `Advanced Usage <https://python-emails.readthedocs.io/en/latest/advanced.html>`_
- Learn the full public API:
  `API Reference <https://python-emails.readthedocs.io/en/latest/api.html>`_
- Troubleshoot common scenarios:
  `FAQ <https://python-emails.readthedocs.io/en/latest/faq.html>`_
- Explore alternatives and related projects:
  `Links <https://python-emails.readthedocs.io/en/latest/links.html>`_


What You Get
------------

- Message composition for HTML, plain text, headers, CC/BCC, and Reply-To
- Attachments, inline images, and MIME generation
- Template rendering in ``html``, ``text``, and ``subject``
- HTML transformations through ``message.transform()``
- SMTP delivery through config dicts or reusable backend objects
- Django integration via ``DjangoMessage``
- Flask integration via `flask-emails <https://github.com/lavr/flask-emails>`_


When To Use It
--------------

Use ``python-emails`` when you need more than a minimal plain-text SMTP call:
HTML emails, attachments, inline images, template rendering, DKIM, message
loading from external sources, or a cleaner API than hand-written
``email.mime`` code.

If you only need to send a very small plain-text message and want zero
dependencies, the standard library may be enough.


Documentation
-------------

- `Documentation home <https://python-emails.readthedocs.io/>`_
- `Quickstart <https://python-emails.readthedocs.io/en/latest/quickstart.html>`_
- `Advanced Usage <https://python-emails.readthedocs.io/en/latest/advanced.html>`_
- `API Reference <https://python-emails.readthedocs.io/en/latest/api.html>`_
- `FAQ <https://python-emails.readthedocs.io/en/latest/faq.html>`_


Project Status
--------------

``python-emails`` is production/stable software and currently supports
Python 3.10 through 3.14.


Contributing
------------

Issues and pull requests are welcome.

- `Report a bug or request a feature <https://github.com/lavr/python-emails/issues>`_
- `Source code on GitHub <https://github.com/lavr/python-emails>`_
- `How to Help <https://python-emails.readthedocs.io/en/latest/howtohelp.html>`_


License
-------

Apache 2.0. See `LICENSE <https://github.com/lavr/python-emails/blob/master/LICENSE>`_.


================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
PAPER         =
BUILDDIR      = _build

# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif

# Internal variables.
PAPEROPT_a4     = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext

help:
	@echo "Please use \`make <target>' where <target> is one of"
	@echo "  html       to make standalone HTML files"
	@echo "  dirhtml    to make HTML files named index.html in directories"
	@echo "  singlehtml to make a single large HTML file"
	@echo "  pickle     to make pickle files"
	@echo "  json       to make JSON files"
	@echo "  htmlhelp   to make HTML files and a HTML help project"
	@echo "  qthelp     to make HTML files and a qthelp project"
	@echo "  devhelp    to make HTML files and a Devhelp project"
	@echo "  epub       to make an epub"
	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
	@echo "  text       to make text files"
	@echo "  man        to make manual pages"
	@echo "  texinfo    to make Texinfo files"
	@echo "  info       to make Texinfo files and run them through makeinfo"
	@echo "  gettext    to make PO message catalogs"
	@echo "  changes    to make an overview of all changed/added/deprecated items"
	@echo "  xml        to make Docutils-native XML files"
	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
	@echo "  linkcheck  to check all external links for integrity"
	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"

clean:
	rm -rf $(BUILDDIR)/*

html:
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

dirhtml:
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."

singlehtml:
	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
	@echo
	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."

pickle:
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
	@echo
	@echo "Build finished; now you can process the pickle files."

json:
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
	@echo
	@echo "Build finished; now you can process the JSON files."

htmlhelp:
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
	@echo
	@echo "Build finished; now you can run HTML Help Workshop with the" \
	      ".hhp project file in $(BUILDDIR)/htmlhelp."

qthelp:
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
	@echo
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-emails.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-emails.qhc"

devhelp:
	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
	@echo
	@echo "Build finished."
	@echo "To view the help file:"
	@echo "# mkdir -p $$HOME/.local/share/devhelp/python-emails"
	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-emails"
	@echo "# devhelp"

epub:
	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
	@echo
	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."

latex:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
	@echo "Run \`make' in that directory to run these through (pdf)latex" \
	      "(use \`make latexpdf' here to do that automatically)."

latexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through pdflatex..."
	$(MAKE) -C $(BUILDDIR)/latex all-pdf
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

latexpdfja:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through platex and dvipdfmx..."
	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

text:
	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
	@echo
	@echo "Build finished. The text files are in $(BUILDDIR)/text."

man:
	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
	@echo
	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."

texinfo:
	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
	@echo
	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
	@echo "Run \`make' in that directory to run these through makeinfo" \
	      "(use \`make info' here to do that automatically)."

info:
	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
	@echo "Running Texinfo files through makeinfo..."
	make -C $(BUILDDIR)/texinfo info
	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."

gettext:
	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
	@echo
	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."

changes:
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
	@echo
	@echo "The overview file is in $(BUILDDIR)/changes."

linkcheck:
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
	@echo
	@echo "Link check complete; look for any errors in the above output " \
	      "or in $(BUILDDIR)/linkcheck/output.txt."

doctest:
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
	@echo "Testing of doctests in the sources finished, look at the " \
	      "results in $(BUILDDIR)/doctest/output.txt."

xml:
	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
	@echo
	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."

pseudoxml:
	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
	@echo
	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."


================================================
FILE: docs/_static/.gitkeep
================================================


================================================
FILE: docs/advanced.rst
================================================
Advanced Usage
==============

This section covers advanced features and usage patterns of ``python-emails``.


SMTP Connections
----------------

By default, :meth:`~emails.Message.send` accepts an ``smtp`` dict and
manages the connection internally:

.. code-block:: python

    response = message.send(
        to="user@example.com",
        smtp={"host": "smtp.example.com", "port": 587, "tls": True,
              "user": "me", "password": "secret"}
    )

For more control, you can use :class:`~emails.backend.smtp.SMTPBackend`
directly.


Reusing Connections
~~~~~~~~~~~~~~~~~~~

When you call :meth:`~emails.Message.send` with the same ``smtp`` dict
on the same message, the library automatically reuses the SMTP connection
through an internal pool. Connections with identical parameters share
a backend:

.. code-block:: python

    smtp_config = {"host": "smtp.example.com", "port": 587, "tls": True,
                   "user": "me", "password": "secret"}

    # These two calls reuse the same underlying SMTP connection
    message.send(to="alice@example.com", smtp=smtp_config)
    message.send(to="bob@example.com", smtp=smtp_config)

For explicit connection management, create an :class:`SMTPBackend` instance
and pass it instead of a dict. The backend supports context managers:

.. code-block:: python

    from emails.backend.smtp import SMTPBackend

    with SMTPBackend(host="smtp.example.com", port=587,
                     tls=True, user="me", password="secret") as backend:
        for recipient in recipients:
            message.send(to=recipient, smtp=backend)
    # Connection is closed automatically


SSL vs STARTTLS
~~~~~~~~~~~~~~~

The library supports two encryption modes:

- **Implicit SSL** (``ssl=True``): Connects over TLS from the start.
  Typically used with port 465.

  .. code-block:: python

      message.send(smtp={"host": "mail.example.com", "port": 465, "ssl": True,
                         "user": "me", "password": "secret"})

- **STARTTLS** (``tls=True``): Connects in plain text, then upgrades to TLS.
  Typically used with port 587.

  .. code-block:: python

      message.send(smtp={"host": "smtp.example.com", "port": 587, "tls": True,
                         "user": "me", "password": "secret"})

You cannot set both ``ssl`` and ``tls`` to ``True`` -- this raises a
``ValueError``.


Timeouts
~~~~~~~~

The default socket timeout is 5 seconds. You can change it with the
``timeout`` parameter:

.. code-block:: python

    message.send(smtp={"host": "smtp.example.com", "timeout": 30})


Debugging
~~~~~~~~~

Enable SMTP protocol debugging to see the full conversation with the
server on stdout:

.. code-block:: python

    message.send(smtp={"host": "smtp.example.com", "debug": 1})


All SMTP Parameters
~~~~~~~~~~~~~~~~~~~

The full list of parameters accepted in the ``smtp`` dict (or as
:class:`SMTPBackend` constructor arguments):

- ``host`` -- SMTP server hostname
- ``port`` -- server port (int)
- ``ssl`` -- use implicit SSL/TLS (for port 465)
- ``tls`` -- use STARTTLS (for port 587)
- ``user`` -- authentication username
- ``password`` -- authentication password
- ``timeout`` -- socket timeout in seconds (default: ``5``)
- ``debug`` -- debug level (``0`` = off, ``1`` = verbose)
- ``fail_silently`` -- if ``True`` (default), return errors in the response
  instead of raising exceptions
- ``local_hostname`` -- FQDN for the EHLO/HELO command (auto-detected
  if not set)
- ``keyfile`` -- path to SSL key file
- ``certfile`` -- path to SSL certificate file
- ``mail_options`` -- list of ESMTP MAIL command options
  (e.g., ``["smtputf8"]``)


HTML Transformations
--------------------

The :meth:`~emails.Message.transform` method processes the HTML body
before sending -- inlining CSS, loading images, removing unsafe tags,
and more.

.. code-block:: python

    message = emails.Message(
        html="<style>h1{color:red}</style><h1>Hello!</h1>"
    )
    message.transform()

After transformation, the inline style is applied directly:

.. code-block:: python

    print(message.html)
    # <html><head></head><body><h1 style="color:red">Hello!</h1></body></html>


Parameters
~~~~~~~~~~

:meth:`~emails.Message.transform` accepts the following keyword arguments:

``css_inline`` (default: ``True``)
    Inline CSS styles using `premailer <https://github.com/peterbe/premailer>`_.
    External stylesheets referenced in ``<link>`` tags are loaded and
    converted to inline ``style`` attributes.

``remove_unsafe_tags`` (default: ``True``)
    Remove potentially dangerous HTML tags: ``<script>``, ``<object>``,
    ``<iframe>``, ``<frame>``, ``<base>``, ``<meta>``, ``<link>``,
    ``<style>``.

``set_content_type_meta`` (default: ``True``)
    Add a ``<meta http-equiv="Content-Type">`` tag to the ``<head>``
    with the message's charset.

``load_images`` (default: ``True``)
    Load images referenced in the HTML and embed them as message
    attachments. Accepts ``True``, ``False``, or a callable for custom
    filtering (see below).

``images_inline`` (default: ``False``)
    When ``True``, loaded images are embedded as inline attachments
    using ``cid:`` references instead of regular attachments.

The following parameters are **deprecated** and have no effect:

``make_links_absolute``
    Premailer always makes links absolute. Passing ``False`` triggers
    a ``DeprecationWarning``.

``update_stylesheet``
    Premailer does not support this feature. Passing ``True`` triggers
    a ``DeprecationWarning``.


Custom Image Filtering
~~~~~~~~~~~~~~~~~~~~~~

Pass a callable as ``load_images`` to control which images are loaded:

.. code-block:: python

    def should_load(element, hints=None, **kwargs):
        # Skip tracking pixels
        src = element.attrib.get("src", "")
        if "track" in src or "pixel" in src:
            return False
        return True

    message.transform(load_images=should_load)

You can also use the ``data-emails`` attribute in your HTML to control
individual images:

- ``data-emails="ignore"`` -- skip loading this image
- ``data-emails="inline"`` -- load as an inline attachment


Custom Link and Image Transformations
--------------------------------------

For more specific transformations, access the ``transformer`` property
directly.


Transforming Image URLs
~~~~~~~~~~~~~~~~~~~~~~~

:meth:`~emails.transformer.HTMLParser.apply_to_images` applies a function
to all image references in the HTML -- ``<img src>``, ``background``
attributes, and CSS ``url()`` values in ``style`` attributes:

.. code-block:: python

    message = emails.Message(html='<img src="promo.png">')
    message.transformer.apply_to_images(
        func=lambda src, **kw: "https://cdn.example.com/images/" + src
    )
    message.transformer.save()

    print(message.html)
    # <html><body><img src="https://cdn.example.com/images/promo.png"></body></html>

The callback receives ``uri`` (the current URL) and ``element`` (the lxml
element), and should return the new URL.

You can limit the scope with keyword arguments:

- ``images=True`` -- apply to ``<img src>`` (default: ``True``)
- ``backgrounds=True`` -- apply to ``background`` attributes (default: ``True``)
- ``styles_uri=True`` -- apply to CSS ``url()`` in style attributes (default: ``True``)


Transforming Link URLs
~~~~~~~~~~~~~~~~~~~~~~

:meth:`~emails.transformer.HTMLParser.apply_to_links` applies a function
to all ``<a href>`` values:

.. code-block:: python

    message = emails.Message(html='<a href="/about">About</a>')
    message.transformer.apply_to_links(
        func=lambda href, **kw: "https://example.com" + href
    )
    message.transformer.save()

    print(message.html)
    # <html><body><a href="https://example.com/about">About</a></body></html>

Always call ``message.transformer.save()`` after using ``apply_to_images``
or ``apply_to_links`` to update the message's HTML body.


Making Images Inline Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can mark individual attachments as inline and synchronize the HTML
references:

.. code-block:: python

    message = emails.Message(html='<img src="promo.png">')
    message.attach(filename="promo.png", data=open("promo.png", "rb"))
    message.attachments["promo.png"].is_inline = True
    message.transformer.synchronize_inline_images()
    message.transformer.save()

    print(message.html)
    # <html><body><img src="cid:promo.png"></body></html>


Loaders
-------

Loader functions create :class:`Message` instances from various sources,
automatically handling HTML parsing, CSS inlining, and image embedding.
All loaders are in the ``emails.loader`` module.


Loading from a URL
~~~~~~~~~~~~~~~~~~

:func:`~emails.loader.from_url` fetches an HTML page and embeds all
referenced images and stylesheets:

.. code-block:: python

    import emails.loader

    message = emails.loader.from_url(
        url="https://example.com/newsletter/2024-01/index.html",
        requests_params={"timeout": 30}
    )

The ``requests_params`` dict is passed to the underlying HTTP requests
(for controlling timeouts, SSL verification, headers, etc.).


Loading from a ZIP Archive
~~~~~~~~~~~~~~~~~~~~~~~~~~

:func:`~emails.loader.from_zip` reads an HTML file and its resources
from a ZIP archive. The archive must contain at least one ``.html`` file:

.. code-block:: python

    message = emails.loader.from_zip(
        open("template.zip", "rb"),
        message_params={"subject": "Newsletter", "mail_from": "news@example.com"}
    )


Loading from a Directory
~~~~~~~~~~~~~~~~~~~~~~~~

:func:`~emails.loader.from_directory` loads from a local directory.
It looks for ``index.html`` (or ``index.htm``) automatically:

.. code-block:: python

    message = emails.loader.from_directory(
        "/path/to/email-template/",
        message_params={"subject": "Welcome", "mail_from": "hello@example.com"}
    )


Loading from a File
~~~~~~~~~~~~~~~~~~~

:func:`~emails.loader.from_file` loads from a single HTML file. Images
and CSS are resolved relative to the file's directory:

.. code-block:: python

    message = emails.loader.from_file("/path/to/email-template/welcome.html")


Loading from an .eml File
~~~~~~~~~~~~~~~~~~~~~~~~~

:func:`~emails.loader.from_rfc822` parses an RFC 822 email (e.g., a
``.eml`` file). Set ``parse_headers=True`` to copy Subject, From, To,
and other headers:

.. code-block:: python

    message = emails.loader.from_rfc822(
        open("archived.eml", "rb").read(),
        parse_headers=True
    )

This loader is primarily intended for demonstration and testing purposes.


When to Use Which Loader
~~~~~~~~~~~~~~~~~~~~~~~~~

- **from_html** -- you already have HTML as a string and want to
  process it (inline CSS, embed images)
- **from_url** -- the email template is hosted on a web server
- **from_directory** -- the template is a local folder with HTML, images,
  and CSS files
- **from_zip** -- the template is distributed as a ZIP archive
- **from_file** -- you have a single local HTML file
- **from_rfc822** -- you want to re-create a message from an existing
  ``.eml`` file


Django Integration
------------------

``python-emails`` provides :class:`~emails.django.DjangoMessage`, a
:class:`Message` subclass that sends through Django's email backend.

.. code-block:: python

    from emails.django import DjangoMessage

    message = DjangoMessage(
        html="<p>Hello {{ name }}!</p>",
        subject="Welcome",
        mail_from="noreply@example.com"
    )
    result = message.send(to="user@example.com", context={"name": "Alice"})

Key differences from :class:`Message`:

- Uses ``context`` instead of ``render`` for template variables.
- Uses Django's configured email backend (``django.core.mail.get_connection()``)
  instead of an ``smtp`` dict.
- Returns ``1`` on success and ``0`` on failure (matching Django's
  ``send_mail`` convention).
- Accepts an optional ``connection`` parameter for a custom Django email
  backend connection.

Using a custom Django connection:

.. code-block:: python

    from django.core.mail import get_connection
    from emails.django import DjangoMessage

    message = DjangoMessage(
        html="<p>Notification</p>",
        subject="Alert",
        mail_from="alerts@example.com"
    )

    connection = get_connection(backend="django.core.mail.backends.smtp.EmailBackend")
    message.send(to="admin@example.com", connection=connection)

Django email settings (``EMAIL_HOST``, ``EMAIL_PORT``, etc.) are used
automatically when no explicit connection is provided.


Flask Integration
-----------------

For Flask applications, use the
`flask-emails <https://github.com/lavr/flask-emails>`_ extension, which
provides Flask-specific integration (app factory support, configuration
from Flask config, etc.):

.. code-block:: python

    from flask_emails import Message

    message = Message(
        html="<p>Hello!</p>",
        subject="Test",
        mail_from="sender@example.com"
    )
    message.send(to="user@example.com")

Install with::

    pip install flask-emails

Refer to the `flask-emails documentation <https://github.com/lavr/flask-emails>`_
for configuration details.


Charset and Encoding
--------------------

``python-emails`` uses two separate encoding settings:

- ``charset`` -- encoding for the message body (default: ``'utf-8'``)
- ``headers_encoding`` -- encoding for email headers (default: ``'ascii'``)


Changing the Body Charset
~~~~~~~~~~~~~~~~~~~~~~~~~

For messages in specific encodings (e.g., Cyrillic), set the ``charset``
parameter:

.. code-block:: python

    message = emails.html(
        html="<p>Content in specific encoding</p>",
        charset="windows-1251",
        mail_from="sender@example.com"
    )

The library automatically registers proper encoding behaviors for common
charsets including ``utf-8``, ``windows-1251``, and ``koi8-r``.


Internationalized Domain Names (IDN)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Email addresses with internationalized domain names work with the
standard address format. The library handles encoding automatically:

.. code-block:: python

    message = emails.html(
        html="<p>Hello!</p>",
        mail_from=("Sender", "user@example.com"),
        mail_to=("Recipient", "user@example.com")
    )


Headers
-------

Custom Headers
~~~~~~~~~~~~~~

Pass a ``headers`` dict when creating a message to add custom email
headers:

.. code-block:: python

    message = emails.html(
        html="<p>Hello!</p>",
        subject="Test",
        mail_from="sender@example.com",
        headers={
            "X-Mailer": "python-emails",
            "X-Priority": "1",
            "List-Unsubscribe": "<mailto:unsubscribe@example.com>"
        }
    )

Non-ASCII characters in header values are automatically encoded
according to RFC 2047.

Header values are validated -- newline characters (``\n``, ``\r``)
raise :exc:`~emails.BadHeaderError` to prevent header injection attacks.


Reply-To, CC, and BCC
~~~~~~~~~~~~~~~~~~~~~~

These fields accept the same formats as ``mail_from`` and ``mail_to`` --
a string, a ``(name, email)`` tuple, or a list of either:

.. code-block:: python

    message = emails.html(
        html="<p>Hello!</p>",
        subject="Team update",
        mail_from=("Alice", "alice@example.com"),
        mail_to=[("Bob", "bob@example.com"), ("Carol", "carol@example.com")],
        cc="dave@example.com",
        bcc=["eve@example.com", "frank@example.com"],
        reply_to=("Alice", "alice-reply@example.com")
    )

- **CC** recipients are visible to all recipients in the email headers.
- **BCC** recipients receive the message but are not listed in the headers.
- **Reply-To** sets the address that email clients use when the recipient
  clicks "Reply".


================================================
FILE: docs/api.rst
================================================
API Reference
=============

This section documents the public API of the ``python-emails`` library.


Message
-------

The :class:`Message` class is the main entry point for creating and sending emails.

.. class:: Message(\*\*kwargs)

   Create a new email message.

   :param html: HTML body content (string or file-like object).
   :type html: str or None
   :param text: Plain text body content (string or file-like object).
   :type text: str or None
   :param subject: Email subject line. Supports template rendering.
   :type subject: str or None
   :param mail_from: Sender address. Accepts a string ``"user@example.com"`` or
       a tuple ``("Display Name", "user@example.com")``.
   :type mail_from: str or tuple or None
   :param mail_to: Recipient address(es). Accepts a string, tuple, or list of strings/tuples.
   :type mail_to: str or tuple or list or None
   :param cc: CC recipient(s). Same format as ``mail_to``.
   :type cc: str or tuple or list or None
   :param bcc: BCC recipient(s). Same format as ``mail_to``.
   :type bcc: str or tuple or list or None
   :param reply_to: Reply-To address(es). Same format as ``mail_to``.
   :type reply_to: str or tuple or list or None
   :param headers: Custom email headers as a dictionary.
   :type headers: dict or None
   :param headers_encoding: Encoding for email headers (default: ``'ascii'``).
   :type headers_encoding: str or None
   :param attachments: List of attachments (dicts or :class:`BaseFile` objects).
   :type attachments: list or None
   :param charset: Message character set (default: ``'utf-8'``).
   :type charset: str or None
   :param message_id: Message-ID header value. Can be a string, a :class:`MessageID` instance,
       ``False`` to omit, or ``None`` to auto-generate.
   :type message_id: str or :class:`~emails.utils.MessageID` or bool or None
   :param date: Date header value. Accepts a string, :class:`~datetime.datetime`, float (timestamp),
       ``False`` to omit, or a callable that returns one of these types.
   :type date: str or datetime or float or bool or callable or None

   Example::

       import emails

       msg = emails.Message(
           html="<p>Hello, World!</p>",
           subject="Test Email",
           mail_from=("Sender", "sender@example.com"),
           mail_to="recipient@example.com"
       )


Message Methods
~~~~~~~~~~~~~~~

.. method:: Message.send(to=None, set_mail_to=True, mail_from=None, set_mail_from=False, render=None, smtp_mail_options=None, smtp_rcpt_options=None, smtp=None)

   Send the message via SMTP.

   :param to: Override recipient address(es).
   :param set_mail_to: If ``True``, update the message's ``mail_to`` with ``to``.
   :param mail_from: Override sender address.
   :param set_mail_from: If ``True``, update the message's ``mail_from``.
   :param render: Dictionary of template variables for rendering.
   :param smtp_mail_options: SMTP MAIL command options.
   :param smtp_rcpt_options: SMTP RCPT command options.
   :param smtp: SMTP configuration. Either a dict with connection parameters
       (``host``, ``port``, ``ssl``, ``tls``, ``user``, ``password``, ``timeout``)
       or an :class:`SMTPBackend` instance.
   :returns: :class:`SMTPResponse` or ``None``

   Example::

       response = msg.send(
           to="user@example.com",
           smtp={"host": "smtp.example.com", "port": 587, "tls": True,
                 "user": "login", "password": "secret"}
       )

.. method:: Message.send_async(to=None, set_mail_to=True, mail_from=None, set_mail_from=False, render=None, smtp_mail_options=None, smtp_rcpt_options=None, smtp=None)

   Send the message via SMTP asynchronously. Requires ``aiosmtplib``
   (install with ``pip install "emails[async]"``).

   Parameters are the same as :meth:`send`, except ``smtp`` accepts a dict
   or an :class:`AsyncSMTPBackend` instance.

   When ``smtp`` is a dict, a temporary :class:`AsyncSMTPBackend` is created
   and closed after sending. When an existing backend is passed, the caller
   is responsible for closing it.

   :returns: :class:`SMTPResponse` or ``None``

   Example::

       response = await msg.send_async(
           to="user@example.com",
           smtp={"host": "smtp.example.com", "port": 587, "tls": True,
                 "user": "login", "password": "secret"}
       )

   Using a shared backend for multiple sends::

       from emails.backend.smtp.aio_backend import AsyncSMTPBackend

       async with AsyncSMTPBackend(host="smtp.example.com", port=587,
                                   tls=True, user="login",
                                   password="secret") as backend:
           for msg in messages:
               await msg.send_async(smtp=backend)

.. method:: Message.attach(\*\*kwargs)

   Attach a file to the message. Sets ``content_disposition`` to ``'attachment'``
   by default.

   :param filename: Name of the attached file.
   :param data: File content as bytes or a file-like object.
   :param content_disposition: ``'attachment'`` (default) or ``'inline'``.
   :param mime_type: MIME type of the file. Auto-detected from filename if not specified.

   Example::

       msg.attach(filename="report.pdf", data=open("report.pdf", "rb"))
       msg.attach(filename="logo.png", data=img_data, content_disposition="inline")

.. method:: Message.render(\*\*kwargs)

   Set template rendering data. Template variables are substituted when
   accessing ``html_body``, ``text_body``, or ``subject``.

   :param kwargs: Key-value pairs used as template context.

   Example::

       msg = emails.Message(
           html=emails.template.JinjaTemplate("<p>Hello {{ name }}</p>"),
           subject=emails.template.JinjaTemplate("Welcome, {{ name }}")
       )
       msg.render(name="World")

.. method:: Message.as_string(message_cls=None)

   Return the message as a string, including DKIM signature if configured.

   :param message_cls: Optional custom MIME message class.
   :returns: Message as a string.
   :rtype: str

.. method:: Message.as_bytes(message_cls=None)

   Return the message as bytes, including DKIM signature if configured.

   :param message_cls: Optional custom MIME message class.
   :returns: Message as bytes.
   :rtype: bytes

.. method:: Message.as_message(message_cls=None)

   Return the underlying MIME message object.

   :param message_cls: Optional custom MIME message class.
   :returns: MIME message object.

.. method:: Message.transform(\*\*kwargs)

   Apply HTML transformations to the message body. Loads and processes the HTML
   content through the transformer.

   See the HTML Transformations section for available parameters.

.. method:: Message.dkim(key, domain, selector, ignore_sign_errors=False, \*\*kwargs)

   Configure DKIM signing for the message. The signature is applied when
   the message is serialized via :meth:`as_string`, :meth:`as_bytes`,
   or :meth:`send`.

   This method is also available as :meth:`sign`.

   :param key: Private key for signing (PEM format). String, bytes, or file-like object.
   :param domain: DKIM domain (e.g., ``"example.com"``).
   :param selector: DKIM selector (e.g., ``"default"``).
   :param ignore_sign_errors: If ``True``, suppress signing exceptions.
   :returns: The message instance (for chaining).
   :rtype: Message

   Example::

       msg.dkim(key=open("private.pem"), domain="example.com", selector="default")


Message Properties
~~~~~~~~~~~~~~~~~~

.. attribute:: Message.html

   Get or set the HTML body content.

.. attribute:: Message.text

   Get or set the plain text body content.

.. attribute:: Message.html_body

   The rendered HTML body (read-only). If templates are used, returns the
   rendered result; otherwise returns the raw HTML.

.. attribute:: Message.text_body

   The rendered text body (read-only). If templates are used, returns the
   rendered result; otherwise returns the raw text.

.. attribute:: Message.mail_from

   Get or set the sender address. Returns a ``(name, email)`` tuple.

.. attribute:: Message.mail_to

   Get or set the recipient address(es). Returns a list of ``(name, email)`` tuples.

.. attribute:: Message.cc

   Get or set CC recipient(s). Returns a list of ``(name, email)`` tuples.

.. attribute:: Message.bcc

   Get or set BCC recipient(s). Returns a list of ``(name, email)`` tuples.

.. attribute:: Message.reply_to

   Get or set Reply-To address(es). Returns a list of ``(name, email)`` tuples.

.. attribute:: Message.subject

   Get or set the email subject. Supports template rendering.

.. attribute:: Message.message_id

   Get or set the Message-ID header value.

.. attribute:: Message.date

   Get or set the Date header value.

.. attribute:: Message.charset

   Get or set the message character set (default: ``'utf-8'``).

.. attribute:: Message.headers_encoding

   Get or set the encoding for email headers (default: ``'ascii'``).

.. attribute:: Message.attachments

   Access the attachment store (:class:`MemoryFileStore`). Lazily initialized.

.. attribute:: Message.render_data

   Get or set the template rendering context dictionary.

.. attribute:: Message.transformer

   Access the HTML transformer for custom image/link transformations.
   Lazily created on first access. See the :doc:`HTML Transformations <transformations>`
   section for usage examples.


SMTPResponse
------------

Returned by :meth:`Message.send`. Contains information about the SMTP transaction.

.. class:: SMTPResponse

   .. attribute:: status_code

      The SMTP status code from the last command (e.g., ``250`` for success).
      ``None`` if the transaction was not completed.

   .. attribute:: status_text

      The SMTP status text from the last command, as bytes.

   .. attribute:: success

      ``True`` if the message was sent successfully (status code is ``250``
      and the transaction completed).

   .. attribute:: error

      The exception object if an error occurred, or ``None``.

   .. attribute:: refused_recipients

      A dictionary mapping refused recipient email addresses to
      ``(code, message)`` tuples.

   .. attribute:: last_command

      The last SMTP command that was sent (e.g., ``'mail'``, ``'rcpt'``, ``'data'``).

   Example::

       response = msg.send(smtp={"host": "localhost"})
       if response.success:
           print("Sent!")
       else:
           print(f"Failed: {response.status_code} {response.status_text}")
           if response.error:
               print(f"Error: {response.error}")
           if response.refused_recipients:
               print(f"Refused: {response.refused_recipients}")


AsyncSMTPBackend
----------------

For async sending via :meth:`Message.send_async`. Requires ``aiosmtplib``
(install with ``pip install "emails[async]"``).

.. class:: emails.backend.smtp.aio_backend.AsyncSMTPBackend(ssl=False, fail_silently=True, mail_options=None, \*\*kwargs)

   Manages an async SMTP connection. Supports ``async with`` for automatic cleanup.

   :param host: SMTP server hostname.
   :param port: SMTP server port.
   :param ssl: Use implicit TLS (SMTPS).
   :param tls: Use STARTTLS after connecting.
   :param user: SMTP username for authentication.
   :param password: SMTP password for authentication.
   :param timeout: Connection timeout in seconds (default: ``5``).
   :param fail_silently: If ``True`` (default), SMTP errors are captured in the
       response rather than raised.
   :param mail_options: Default SMTP MAIL command options.

   .. method:: sendmail(from_addr, to_addrs, msg, mail_options=None, rcpt_options=None)
      :async:

      Send a message. Automatically retries once on server disconnect.

      :returns: :class:`SMTPResponse` or ``None``

   .. method:: close()
      :async:

      Close the SMTP connection.

   Example::

       from emails.backend.smtp.aio_backend import AsyncSMTPBackend

       async with AsyncSMTPBackend(host="smtp.example.com", port=587,
                                   tls=True, user="me",
                                   password="secret") as backend:
           response = await backend.sendmail(
               from_addr="sender@example.com",
               to_addrs=["recipient@example.com"],
               msg=message
           )


Loaders
-------

Loader functions create :class:`Message` instances from various sources.

All loaders are available in the ``emails.loader`` module.

.. function:: emails.loader.from_html(html, text=None, base_url=None, message_params=None, local_loader=None, template_cls=None, message_cls=None, source_filename=None, requests_params=None, \*\*kwargs)

   Create a message from an HTML string. Images and stylesheets referenced
   in the HTML can be automatically loaded and embedded.

   :param html: HTML content as a string.
   :param text: Optional plain text alternative.
   :param base_url: Base URL for resolving relative URLs in the HTML.
   :param message_params: Additional parameters passed to the Message constructor.
   :param local_loader: A loader instance for resolving local file references.
   :param template_cls: Template class to use for the HTML body.
   :param message_cls: Custom Message class to instantiate.
   :param source_filename: Filename hint for the source HTML.
   :param requests_params: Parameters passed to HTTP requests when fetching resources.
   :param kwargs: Additional transformer options.
   :returns: A :class:`Message` instance.

   ``from_string`` is an alias for this function.

.. function:: emails.loader.from_url(url, requests_params=None, \*\*kwargs)

   Create a message by downloading an HTML page from a URL.
   Images and stylesheets are fetched and embedded.

   :param url: URL of the HTML page.
   :param requests_params: Parameters passed to HTTP requests.
   :param kwargs: Additional transformer options.
   :returns: A :class:`Message` instance.

   ``load_url`` is an alias for this function.

.. function:: emails.loader.from_directory(directory, loader_cls=None, \*\*kwargs)

   Create a message from a local directory. The directory should contain
   an HTML file and any referenced images or attachments.

   :param directory: Path to the directory.
   :param loader_cls: Custom loader class.
   :param kwargs: Additional options (``html_filename``, ``text_filename``, ``message_params``).
   :returns: A :class:`Message` instance.

.. function:: emails.loader.from_zip(zip_file, loader_cls=None, \*\*kwargs)

   Create a message from a ZIP archive containing HTML and resources.

   :param zip_file: Path to ZIP file or a file-like object.
   :param loader_cls: Custom loader class.
   :param kwargs: Additional options (``html_filename``, ``text_filename``, ``message_params``).
   :returns: A :class:`Message` instance.

.. function:: emails.loader.from_file(filename, \*\*kwargs)

   Create a message from a single HTML file.

   :param filename: Path to the HTML file.
   :param kwargs: Additional options (``message_params``).
   :returns: A :class:`Message` instance.

.. function:: emails.loader.from_rfc822(msg, loader_cls=None, message_params=None, parse_headers=False)

   Create a message from an RFC 822 email object (e.g., from :mod:`email.message`).
   Primarily intended for demonstration and testing purposes.

   :param msg: An :class:`email.message.Message` object.
   :param loader_cls: Custom loader class.
   :param message_params: Additional parameters for the Message constructor.
   :param parse_headers: If ``True``, parse and transfer email headers.
   :returns: A :class:`Message` instance.


Loader Exceptions
~~~~~~~~~~~~~~~~~

.. exception:: emails.loader.LoadError

   Base exception for all loader errors.

.. exception:: emails.loader.IndexFileNotFound

   Raised when the loader cannot find an HTML index file in the source.
   Subclass of :exc:`LoadError`.

.. exception:: emails.loader.InvalidHtmlFile

   Raised when the HTML content cannot be parsed.
   Subclass of :exc:`LoadError`.


Templates
---------

Template classes allow dynamic content in email bodies and subjects. Pass a
template instance as the ``html``, ``text``, or ``subject`` parameter of
:class:`Message`.

Install template dependencies with extras::

    pip install "emails[jinja]"   # for JinjaTemplate

.. class:: emails.template.JinjaTemplate(template_text, environment=None)

   Template using `Jinja2 <https://jinja.palletsprojects.com/>`_ syntax.

   :param template_text: Jinja2 template string.
   :param environment: Optional :class:`jinja2.Environment` instance.

   Example::

       from emails.template import JinjaTemplate

       msg = emails.Message(
           html=JinjaTemplate("<p>Hello {{ name }}!</p>"),
           subject=JinjaTemplate("Welcome, {{ name }}"),
           mail_from="noreply@example.com"
       )
       msg.send(render={"name": "Alice"}, smtp={"host": "localhost"})

.. class:: emails.template.StringTemplate(template_text, safe_substitute=True)

   Template using Python's :class:`string.Template` syntax (``$variable`` or
   ``${variable}``).

   :param template_text: Template string.
   :param safe_substitute: If ``True`` (default), undefined variables are left
       as-is. If ``False``, undefined variables raise :exc:`KeyError`.

   Example::

       from emails.template import StringTemplate

       msg = emails.Message(
           html=StringTemplate("<p>Hello $name!</p>"),
           mail_from="noreply@example.com"
       )

.. class:: emails.template.MakoTemplate(template_text, \*\*kwargs)

   Template using `Mako <https://www.makotemplates.org/>`_ syntax.
   Requires the ``mako`` package.

   :param template_text: Mako template string.
   :param kwargs: Additional parameters passed to :class:`mako.template.Template`.


DjangoMessage
-------------

A :class:`Message` subclass for use with Django's email backend.

.. class:: emails.django.DjangoMessage(\*\*kwargs)

   Accepts the same parameters as :class:`Message`. Integrates with
   Django's email sending infrastructure.

   .. method:: send(mail_to=None, set_mail_to=True, mail_from=None, set_mail_from=False, context=None, connection=None, to=None)

      Send the message through Django's email backend.

      :param context: Dictionary of template rendering variables
          (equivalent to ``render`` in :meth:`Message.send`).
      :param connection: A Django email backend connection instance
          (e.g., from ``django.core.mail.get_connection()``).
          If ``None``, uses the default backend.
      :param to: Alias for ``mail_to``.
      :returns: ``1`` if the message was sent successfully, ``0`` otherwise.
      :rtype: int

   Example::

       from emails.django import DjangoMessage

       msg = DjangoMessage(
           html="<p>Hello {{ name }}</p>",
           subject="Welcome",
           mail_from="noreply@example.com"
       )
       msg.send(to="user@example.com", context={"name": "Alice"})


DKIM
----

DKIM (DomainKeys Identified Mail) signing is configured via the
:meth:`Message.dkim` method (or its alias :meth:`Message.sign`).

Parameters:

- ``key`` -- Private key in PEM format. Accepts a string, bytes, or file-like object.
- ``domain`` -- The signing domain (e.g., ``"example.com"``).
- ``selector`` -- The DKIM selector (e.g., ``"default"``).
- ``ignore_sign_errors`` -- If ``True``, silently ignore signing errors
  instead of raising exceptions.
- Additional keyword arguments are passed to the DKIM library
  (e.g., ``canonicalize``, ``signature_algorithm``).

Returns the message instance (for chaining).

Example::

    import emails

    msg = emails.Message(
        html="<p>Signed message</p>",
        mail_from=("Sender", "sender@example.com"),
        subject="DKIM Test"
    )
    msg.dkim(
        key=open("private.pem").read(),
        domain="example.com",
        selector="default"
    )
    msg.send(to="recipient@example.com", smtp={"host": "localhost"})

The signature is automatically applied when the message is serialized
(via :meth:`~Message.as_string`, :meth:`~Message.as_bytes`, or :meth:`~Message.send`).


Exceptions
----------

.. exception:: emails.HTTPLoaderError

   Raised when loading content from a URL fails (e.g., HTTP error, connection timeout).

.. exception:: emails.BadHeaderError

   Raised when an email header contains invalid characters (such as newlines
   or carriage returns).

.. exception:: emails.IncompleteMessage

   Raised when attempting to send a message that lacks required content
   (no HTML and no text body).

See also the `Loader Exceptions`_ section for loader-specific exceptions:
``LoadError``, ``IndexFileNotFound``, ``InvalidHtmlFile``.


Utilities
---------

.. class:: emails.utils.MessageID(domain=None, idstring=None)

   Generator for RFC 2822 compliant Message-ID values.

   :param domain: Domain part of the Message-ID. Defaults to the machine's FQDN.
   :param idstring: Optional additional string to strengthen uniqueness.

   The instance is callable — each call generates a new unique Message-ID.

   Example::

       from emails.utils import MessageID

       # Auto-generate a new Message-ID for each send
       msg = emails.Message(
           message_id=MessageID(domain="example.com"),
           html="<p>Hello</p>",
           mail_from="sender@example.com"
       )

.. function:: emails.html(\*\*kwargs)

   Convenience function that creates and returns a :class:`Message` instance.
   Accepts all the same parameters as the :class:`Message` constructor.

   Example::

       msg = emails.html(
           html="<p>Hello!</p>",
           subject="Test",
           mail_from="sender@example.com"
       )


================================================
FILE: docs/conf.py
================================================
import sys
import os

sys.path.append(os.path.abspath('..'))

# -- General configuration ------------------------------------------------

extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.doctest',
    'sphinx.ext.intersphinx',
    'sphinx.ext.coverage',
    'sphinx.ext.ifconfig',
    'sphinx.ext.viewcode',
    'sphinx_togglebutton',
]

templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'

project = 'python-emails'
copyright = '2015-2026, Sergey Lavrinenko'

from emails import __version__ as _emails_version
version = '.'.join(_emails_version.split('.')[:2])
release = _emails_version

exclude_patterns = ['_build', 'examples.rst']
pygments_style = 'sphinx'

# -- Options for HTML output ----------------------------------------------

html_theme = 'furo'

html_theme_options = {
    "source_repository": "https://github.com/lavr/python-emails",
    "source_branch": "master",
    "source_directory": "docs/",
    "navigation_with_keys": True,
}

html_title = f"python-emails {release}"

html_static_path = ['_static']

htmlhelp_basename = 'python-emailsdoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {}

latex_documents = [
    ('index', 'python-emails.tex', 'python-emails Documentation',
     'Sergey Lavrinenko', 'manual'),
]

# -- Options for manual page output ---------------------------------------

man_pages = [
    ('index', 'python-emails', 'python-emails Documentation',
     ['Sergey Lavrinenko'], 1)
]

# -- Options for Texinfo output -------------------------------------------

texinfo_documents = [
    ('index', 'python-emails', 'python-emails Documentation',
     'Sergey Lavrinenko', 'python-emails',
     'Modern email handling in python.',
     'Miscellaneous'),
]

# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}


================================================
FILE: docs/examples.rst
================================================
Features
--------

-  HTML-email message abstraction
-  Method to transform html body:

   - css inlining (using peterbe's premailer)
   - image inlining
-  DKIM signature
-  Message loaders
-  Send directly or via django email backend


Examples
--------

Create message:

.. code-block:: python

    import emails
    message = emails.html(html=open('letter.html'),
                          subject='Friday party',
                          mail_from=('Company Team', 'contact@mycompany.com'))


Attach files or inline images:

.. code-block:: python

    message.attach(data=open('event.ics', 'rb'), filename='Event.ics')
    message.attach(data=open('image.png', 'rb'), filename='image.png',
                   content_disposition='inline')

Use templates (requires ``pip install "emails[jinja]"``):

.. code-block:: python

    from emails.template import JinjaTemplate as T

    message = emails.html(subject=T('Payment Receipt No.{{ billno }}'),
                          html=T('<p>Dear {{ name }}! This is a receipt...'),
                          mail_from=('ABC', 'robot@mycompany.com'))

    message.send(to=('John Brown', 'jbrown@gmail.com'),
                 render={'name': 'John Brown', 'billno': '141051906163'})



Add DKIM signature:

.. code-block:: python

    message.dkim(key=open('my.key'), domain='mycompany.com', selector='newsletter')

Generate email.message or rfc822 string:

.. code-block:: python

    m = message.as_message()
    s = message.as_string()


Send and get response from smtp server:

.. code-block:: python

    r = message.send(to=('John Brown', 'jbrown@gmail.com'),
                     render={'name': 'John'},
                     smtp={'host':'smtp.mycompany.com', 'port': 465, 'ssl': True, 'user': 'john', 'password': '***'})
    assert r.status_code == 250


Django
------

DjangoMessage helper sends via django configured email backend:

.. code-block:: python

    from emails.django import DjangoMessage as Message
    message = Message(...)
    message.send(mail_to=('John Brown', 'jbrown@gmail.com'),
                 context={'name': 'John'})

Flask
-----

For flask integration take a look at `flask-emails <https://github.com/lavr/flask-emails>`_


================================================
FILE: docs/faq.rst
================================================
FAQ
===

Frequently asked questions about ``python-emails``.


How do I send through Gmail / Yandex / other providers?
-------------------------------------------------------

All SMTP providers follow the same pattern — pass the provider's SMTP
host, port, and credentials in the ``smtp`` dict:

.. code-block:: python

    response = message.send(
        to="recipient@example.com",
        smtp={
            "host": "<provider SMTP host>",
            "port": 587,
            "tls": True,
            "user": "your-email@example.com",
            "password": "your-password-or-app-password"
        }
    )

Common SMTP settings:

.. list-table::
   :header-rows: 1
   :widths: 20 30 10 15

   * - Provider
     - Host
     - Port
     - Encryption
   * - Gmail
     - ``smtp.gmail.com``
     - 587
     - ``tls=True``
   * - Yandex
     - ``smtp.yandex.ru``
     - 465
     - ``ssl=True``
   * - Outlook / Hotmail
     - ``smtp-mail.outlook.com``
     - 587
     - ``tls=True``
   * - Yahoo Mail
     - ``smtp.mail.yahoo.com``
     - 465
     - ``ssl=True``

.. note::

   Most providers require an **app password** instead of your regular
   account password. Consult the provider's documentation:

   - `Gmail: Sign in with app passwords <https://support.google.com/accounts/answer/185833>`_
   - `Yandex: App passwords <https://yandex.com/support/id/authorization/app-passwords.html>`_
   - `Outlook: App passwords <https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944>`_
   - `Yahoo: App passwords <https://help.yahoo.com/kb/generate-manage-third-party-passwords-sln15241.html>`_

   Provider settings and authentication requirements change over time.
   Always refer to the official documentation for up-to-date instructions.


How do I attach a PDF or Excel file?
-------------------------------------

Use :meth:`~emails.Message.attach` with the file's data and filename.
The MIME type is auto-detected from the filename extension:

.. code-block:: python

    # Attach a PDF
    message.attach(filename="report.pdf", data=open("report.pdf", "rb"))

    # Attach an Excel file
    message.attach(filename="data.xlsx", data=open("data.xlsx", "rb"))

    # Attach with an explicit MIME type
    message.attach(
        filename="archive.7z",
        data=open("archive.7z", "rb"),
        mime_type="application/x-7z-compressed"
    )

You can also attach in-memory data:

.. code-block:: python

    import io

    csv_data = "name,score\nAlice,95\nBob,87\n"
    message.attach(
        filename="scores.csv",
        data=io.BytesIO(csv_data.encode("utf-8"))
    )


How is python-emails different from smtplib + email.mime?
---------------------------------------------------------

``python-emails`` is built on top of the standard library's ``email`` and
``smtplib`` modules. The difference is the level of abstraction.

With ``python-emails``:

.. code-block:: python

    import emails
    from emails.template import JinjaTemplate as T

    message = emails.html(
        subject=T("Passed: {{ project_name }}#{{ build_id }}"),
        html=T("<html><p>Build passed: {{ project_name }} "
               "<img src='cid:icon.png'> ...</p></html>"),
        text=T("Build passed: {{ project_name }} ..."),
        mail_from=("CI", "ci@mycompany.com")
    )
    message.attach(filename="icon.png", data=open("icon.png", "rb"),
                   content_disposition="inline")

    message.send(
        to="somebody@mycompany.com",
        render={"project_name": "user/project1", "build_id": 121},
        smtp={"host": "smtp.mycompany.com", "port": 587, "tls": True,
              "user": "ci", "password": "secret"}
    )

The same message with the standard library alone:

.. container:: toggle

    .. code-block:: python

        import os
        import smtplib
        from email.utils import formataddr, formatdate, COMMASPACE
        from email.header import Header
        from email import encoders
        from email.mime.multipart import MIMEMultipart
        from email.mime.base import MIMEBase
        from email.mime.text import MIMEText
        from email.mime.image import MIMEImage
        import jinja2

        sender_name, sender_email = "CI", "ci@mycompany.com"
        recipient_addr = ["somebody@mycompany.com"]

        j = jinja2.Environment()
        ctx = {"project_name": "user/project1", "build_id": 121}
        html = j.from_string(
            "<html><p>Build passed: {{ project_name }} "
            "<img src='cid:icon.png'> ...</p></html>"
        ).render(**ctx)
        text = j.from_string("Build passed: {{ project_name }} ...").render(**ctx)
        subject = j.from_string(
            "Passed: {{ project_name }}#{{ build_id }}"
        ).render(**ctx)

        encoded_name = Header(sender_name, "utf-8").encode()
        msg_root = MIMEMultipart("mixed")
        msg_root["Date"] = formatdate(localtime=True)
        msg_root["From"] = formataddr((encoded_name, sender_email))
        msg_root["To"] = COMMASPACE.join(recipient_addr)
        msg_root["Subject"] = Header(subject, "utf-8")
        msg_root.preamble = "This is a multi-part message in MIME format."

        msg_related = MIMEMultipart("related")
        msg_root.attach(msg_related)
        msg_alternative = MIMEMultipart("alternative")
        msg_related.attach(msg_alternative)

        msg_text = MIMEText(text.encode("utf-8"), "plain", "utf-8")
        msg_alternative.attach(msg_text)
        msg_html = MIMEText(html.encode("utf-8"), "html", "utf-8")
        msg_alternative.attach(msg_html)

        with open("icon.png", "rb") as fp:
            msg_image = MIMEImage(fp.read())
            msg_image.add_header("Content-ID", "<icon.png>")
            msg_related.attach(msg_image)

        mail_server = smtplib.SMTP("smtp.mycompany.com", 587)
        mail_server.ehlo()
        try:
            mail_server.starttls()
            mail_server.ehlo()
        except smtplib.SMTPException as e:
            print(e)
        mail_server.login("ci", "secret")
        mail_server.send_message(msg_root)
        mail_server.quit()

The standard library version requires:

- Manual MIME tree construction (``MIMEMultipart`` nesting of ``mixed``,
  ``related``, and ``alternative`` parts)
- Explicit header encoding with ``Header``
- Manual ``Content-ID`` management for inline images
- Separate template rendering before message assembly
- Direct SMTP session management (``ehlo``, ``starttls``, ``login``,
  ``quit``)

``python-emails`` handles all of this internally.


How is python-emails different from django.core.mail?
-----------------------------------------------------

``django.core.mail`` is Django's built-in email module. It works well
within Django but has several limitations compared to ``python-emails``:

- **No HTML transformations** — ``django.core.mail`` sends HTML as-is.
  ``python-emails`` can inline CSS, embed images, and clean up unsafe
  tags via :meth:`~emails.Message.transform`.
- **No template integration** — with ``django.core.mail`` you render
  templates manually before passing HTML to the message.
  ``python-emails`` accepts template objects directly in ``html``,
  ``text``, and ``subject``.
- **No loaders** — ``python-emails`` can create messages from URLs, ZIP
  archives, directories, and ``.eml`` files.
- **No DKIM** — ``python-emails`` supports DKIM signing out of the box.
- **Django-only** — ``django.core.mail`` requires a Django project.
  ``python-emails`` works in any Python project.

If you are in a Django project and want to use ``python-emails``,
the :class:`~emails.django.DjangoMessage` class integrates with Django's
email backend:

.. code-block:: python

    from emails.django import DjangoMessage

    message = DjangoMessage(
        html="<p>Hello {{ name }}!</p>",
        subject="Welcome",
        mail_from="noreply@example.com"
    )
    message.send(to="user@example.com", context={"name": "Alice"})

See the :doc:`Django Integration <advanced>` section for more details.


How do I debug email sending?
-----------------------------

There are two levels of debugging: SMTP protocol tracing and Python
logging.


SMTP Protocol Trace
~~~~~~~~~~~~~~~~~~~

Set ``debug=1`` in the ``smtp`` dict to print the full SMTP conversation
to stdout:

.. code-block:: python

    response = message.send(
        to="user@example.com",
        smtp={"host": "smtp.example.com", "port": 587, "tls": True,
              "user": "me", "password": "secret", "debug": 1}
    )

This outputs every command and response exchanged with the SMTP server,
which is useful for diagnosing authentication failures, TLS issues, and
rejected recipients.


Python Logging
~~~~~~~~~~~~~~

The library uses Python's standard ``logging`` module. Enable it to see
connection events and retries:

.. code-block:: python

    import logging

    logging.basicConfig(level=logging.DEBUG)

    # Or enable only the emails loggers:
    logging.getLogger("emails.backend.smtp.backend").setLevel(logging.DEBUG)
    logging.getLogger("emails.backend.smtp.client").setLevel(logging.DEBUG)

Logger names used by the library:

- ``emails.backend.smtp.backend`` — connection management, retries
- ``emails.backend.smtp.client`` — SMTP client operations


Inspecting the Message
~~~~~~~~~~~~~~~~~~~~~~

Before sending, you can inspect the raw RFC 822 output:

.. code-block:: python

    print(message.as_string())

This shows the full MIME structure, headers, and encoded content —
useful for verifying that attachments, inline images, and headers are
correct.


Checking the Response
~~~~~~~~~~~~~~~~~~~~~

After sending, inspect the :class:`SMTPResponse` object:

.. code-block:: python

    response = message.send(to="user@example.com", smtp={...})

    print(f"Status: {response.status_code}")
    print(f"Text: {response.status_text}")
    print(f"Success: {response.success}")

    if response.error:
        print(f"Error: {response.error}")

    if response.refused_recipients:
        for addr, (code, reason) in response.refused_recipients.items():
            print(f"Refused {addr}: {code} {reason}")


================================================
FILE: docs/howtohelp.rst
================================================
How to Help
===========

Contributions are welcome! Here is how you can help:

1. `Open an issue <https://github.com/lavr/python-emails/issues>`_ to report a bug or suggest a feature.
2. Fork the repository on GitHub and start making your changes on a new branch.
3. Write a test which shows that the bug was fixed.
4. Send a pull request. Make sure to add yourself to `AUTHORS <https://github.com/lavr/python-emails/blob/master/README.rst>`_.


================================================
FILE: docs/index.rst
================================================
python-emails
=============

.. module:: emails

|pypi| |python| |license|

.. |pypi| image:: https://img.shields.io/pypi/v/emails.svg
   :target: https://pypi.org/project/emails/
   :alt: PyPI version

.. |python| image:: https://img.shields.io/pypi/pyversions/emails.svg
   :target: https://pypi.org/project/emails/
   :alt: Python versions

.. |license| image:: https://img.shields.io/pypi/l/emails.svg
   :target: https://github.com/lavr/python-emails/blob/master/LICENSE
   :alt: License

Modern email handling in python. Build, transform, and send emails with a
clean, intuitive API.

.. code-block:: python

    import emails

    message = emails.html(
        subject="Hi from python-emails!",
        html="<html><p>Hello, <strong>World!</strong></p></html>",
        mail_from=("Alice", "alice@example.com"),
    )
    response = message.send(
        to="bob@example.com",
        smtp={"host": "smtp.example.com", "port": 587, "tls": True},
    )
    assert response.status_code == 250


.. rubric:: Features

- Build HTML and plain-text emails with a simple API
- CSS inlining, image embedding, and HTML cleanup via built-in transformations
- Jinja2, Mako, and string templates for dynamic content
- Inline images and file attachments
- DKIM signing
- Load messages from URLs, HTML files, directories, ZIP archives, or RFC 822 files
- Django integration via ``DjangoMessage``
- SMTP sending with SSL/TLS support
- Async sending via ``aiosmtplib``


.. toctree::
   :maxdepth: 2

   quickstart
   transformations
   advanced
   api
   faq
   install
   howtohelp
   links


================================================
FILE: docs/install.rst
================================================
Install
=======

Install from pypi:

.. code-block:: bash

    $ pip install emails

This installs the lightweight core for building and sending email messages.

To use HTML transformation features (CSS inlining, image embedding, loading from URL/file):

.. code-block:: bash

    $ pip install "emails[html]"

To use Jinja2 templates (the ``T()`` shortcut):

.. code-block:: bash

    $ pip install "emails[jinja]"

To use async sending (``send_async()``):

.. code-block:: bash

    $ pip install "emails[async]"


================================================
FILE: docs/links.rst
================================================
See also
========

Alternatives
------------

There are several Python libraries for sending email, each with a different focus:

- **smtplib + email** (standard library) — built into Python, provides low-level SMTP
  transport and RFC-compliant message construction. Full control, but requires manual
  MIME assembly for HTML emails with attachments. python-emails builds on top of these
  modules and adds a higher-level API with HTML transformations, template support, and
  loaders.

- `yagmail <https://github.com/kootenpv/yagmail>`_ — a friendly Gmail/SMTP client that
  auto-detects content types and simplifies sending. Supports OAuth2 for Gmail and
  optional DKIM signing. A good choice when you need a quick way to send emails with
  minimal setup.

- `red-mail <https://github.com/Miksus/red-mail>`_ — advanced email sending with
  built-in Jinja2 templates, prettified HTML tables, and embedded images. A good fit
  if you need to send data-driven reports.

- `envelope <https://github.com/CZ-NIC/envelope>`_ — an all-in-one library with GPG
  and S/MIME encryption, a fluent Python API, and a CLI interface. The right choice when
  email encryption or signing is a requirement.

python-emails focuses on **HTML email as a first-class citizen**: loading HTML from
URLs, files, ZIP archives, or directories, automatic CSS inlining and image embedding
via built-in transformations, and multiple template engines (Jinja2, Mako, string
templates). It also provides DKIM signing and Django integration out of the box.


Acknowledgements
----------------

python-emails uses `premailer <https://github.com/peterbe/premailer>`_ for CSS inlining
— converting ``<style>`` blocks into inline ``style`` attributes for maximum email client
compatibility.


================================================
FILE: docs/quickstart.rst
================================================
Quickstart
==========

``python-emails`` is a library for composing and sending email messages
in Python. It provides a clean, high-level API over the standard library's
``email`` and ``smtplib`` modules.

With ``smtplib`` and ``email.mime``, sending an HTML email with an attachment
requires assembling MIME parts manually, encoding headers, handling character
sets, and managing the SMTP connection — often 30+ lines of boilerplate for
a simple message. ``python-emails`` reduces that to a few lines:

.. code-block:: python

    import emails

    message = emails.html(
        html="<p>Hello, World!</p>",
        subject="My first email",
        mail_from=("Me", "me@example.com")
    )
    response = message.send(to="you@example.com",
                            smtp={"host": "smtp.example.com", "port": 587,
                                  "tls": True, "user": "me", "password": "secret"})

The library handles MIME structure, character encoding, inline images,
CSS inlining, DKIM signing, and template rendering — things that are tedious
to do correctly with the standard library.


Creating a Message
------------------

The simplest way to create a message is :func:`emails.html`:

.. code-block:: python

    import emails

    message = emails.html(
        html="<h1>Friday party!</h1><p>You are invited.</p>",
        subject="Friday party",
        mail_from=("Company Team", "contact@mycompany.com")
    )

:func:`emails.html` is a shortcut for :class:`~emails.Message` — both accept
the same parameters:

- ``html`` — HTML body content (string or file-like object)
- ``text`` — plain text alternative
- ``subject`` — email subject line
- ``mail_from`` — sender address, as a string ``"user@example.com"`` or
  a tuple ``("Display Name", "user@example.com")``
- ``mail_to`` — recipient(s), same format as ``mail_from``; also accepts a list

You can also set ``cc``, ``bcc``, ``reply_to``, ``headers``, ``charset``,
and other parameters. See the :doc:`API Reference <api>` for full details.

If you have HTML in a file:

.. code-block:: python

    message = emails.html(
        html=open("letter.html"),
        subject="Newsletter",
        mail_from="newsletter@example.com"
    )


Sending a Message
-----------------

Call :meth:`~emails.Message.send` with an ``smtp`` dict describing
your SMTP server:

.. code-block:: python

    response = message.send(
        to="recipient@example.com",
        smtp={
            "host": "smtp.example.com",
            "port": 587,
            "tls": True,
            "user": "me@example.com",
            "password": "secret"
        }
    )

The ``smtp`` dict supports these keys:

- ``host`` — SMTP server hostname (default: ``"localhost"``)
- ``port`` — server port (default: ``25``)
- ``ssl`` — use SSL/TLS connection (for port 465)
- ``tls`` — use STARTTLS (for port 587)
- ``user`` — username for authentication
- ``password`` — password for authentication
- ``timeout`` — connection timeout in seconds (default: ``5``)

:meth:`~emails.Message.send` returns an :class:`SMTPResponse` object.
Check ``status_code`` to verify the message was accepted:

.. code-block:: python

    if response.status_code == 250:
        print("Message sent successfully")
    else:
        print(f"Send failed: {response.status_code}")


Attachments
-----------

Use :meth:`~emails.Message.attach` to add files to a message:

.. code-block:: python

    message.attach(filename="report.pdf", data=open("report.pdf", "rb"))
    message.attach(filename="data.csv", data=open("data.csv", "rb"))

Each attachment gets ``content_disposition='attachment'`` by default,
which means the file appears as a downloadable attachment in the recipient's
email client.

You can specify a MIME type explicitly:

.. code-block:: python

    message.attach(
        filename="event.ics",
        data=open("event.ics", "rb"),
        mime_type="text/calendar"
    )

If ``mime_type`` is not specified, it is auto-detected from the filename.


Inline Images
-------------

Inline images are embedded directly in the HTML body rather than shown as
attachments. They use the ``cid:`` (Content-ID) URI scheme to reference
embedded content.

To use an inline image:

1. Reference it in your HTML with ``cid:filename``
2. Attach it with ``content_disposition="inline"``

.. code-block:: python

    message = emails.html(
        html='<p>Hello! <img src="cid:logo.png"></p>',
        subject="With inline image",
        mail_from="sender@example.com"
    )
    message.attach(
        filename="logo.png",
        data=open("logo.png", "rb"),
        content_disposition="inline"
    )

The ``cid:logo.png`` in the HTML ``src`` attribute tells the email client
to display the attached file named ``logo.png`` inline at that position,
rather than as a separate attachment.


Templates
---------

For emails with dynamic content, use template classes instead of plain strings.
The most common choice is :class:`~emails.template.JinjaTemplate`, which uses
`Jinja2 <https://jinja.palletsprojects.com/>`_ syntax:

.. code-block:: python

    from emails.template import JinjaTemplate as T

    message = emails.html(
        subject=T("Payment Receipt No.{{ bill_no }}"),
        html=T("<p>Dear {{ name }},</p><p>Your payment of ${{ amount }} was received.</p>"),
        mail_from=("Billing", "billing@mycompany.com")
    )

Pass template variables via the ``render`` parameter of :meth:`~emails.Message.send`:

.. code-block:: python

    message.send(
        to="customer@example.com",
        render={"name": "Alice", "bill_no": "12345", "amount": "99.00"},
        smtp={"host": "smtp.example.com", "port": 587, "tls": True,
              "user": "billing", "password": "secret"}
    )

Templates work in ``html``, ``text``, and ``subject`` — all three are rendered
with the same variables.

Jinja2 templates require the ``jinja2`` package. Install it with::

    pip install "emails[jinja]"

Two other template backends are available:

- :class:`~emails.template.StringTemplate` — uses Python's
  :class:`string.Template` syntax (``$variable``)
- :class:`~emails.template.MakoTemplate` — uses
  `Mako <https://www.makotemplates.org/>`_ syntax (requires the ``mako`` package)


DKIM Signing
------------

DKIM (DomainKeys Identified Mail) lets the recipient verify that an email
was authorized by the domain owner. This improves deliverability and reduces
the chance of messages being marked as spam.

To sign a message, call :meth:`~emails.Message.dkim` with your private key,
domain, and selector:

.. code-block:: python

    message.dkim(
        key=open("private.pem", "rb"),
        domain="mycompany.com",
        selector="default"
    )

The signature is applied automatically when the message is sent or serialized.
The method returns the message instance, so you can chain it:

.. code-block:: python

    message = emails.html(
        html="<p>Signed message</p>",
        mail_from="sender@mycompany.com",
        subject="DKIM Test"
    ).dkim(key=open("private.pem", "rb"), domain="mycompany.com", selector="default")

DKIM requires a private key in PEM format and a corresponding DNS TXT record
on your domain. Consult your DNS provider's documentation for setting up
the DNS record.


Generating Without Sending
---------------------------

Sometimes you need the raw email content without actually sending it —
for example, to store it, pass it to another system, or inspect it.

:meth:`~emails.Message.as_string` returns the full RFC 822 message as a string:

.. code-block:: python

    raw = message.as_string()
    print(raw)

:meth:`~emails.Message.as_message` returns a standard library
:class:`email.message.Message` object, which you can inspect or manipulate:

.. code-block:: python

    msg = message.as_message()
    print(msg["Subject"])
    print(msg["From"])

There is also :meth:`~emails.Message.as_bytes` if you need the message
as bytes.

If DKIM signing is configured, the signature is included in the output
of all three methods.


Error Handling
--------------

:meth:`~emails.Message.send` returns an :class:`SMTPResponse` object.
It never raises an exception for SMTP errors by default — instead, error
information is available on the response:

.. code-block:: python

    response = message.send(
        to="recipient@example.com",
        smtp={"host": "smtp.example.com", "port": 587, "tls": True,
              "user": "me", "password": "secret"}
    )

    if response.success:
        print("Sent!")
    else:
        print(f"Failed with status {response.status_code}: {response.status_text}")

        # Check for connection/auth errors
        if response.error:
            print(f"Error: {response.error}")

        # Check for rejected recipients
        if response.refused_recipients:
            for addr, (code, reason) in response.refused_recipients.items():
                print(f"  Refused {addr}: {code} {reason}")

Key attributes of :class:`SMTPResponse`:

- ``success`` — ``True`` if the message was accepted (status code 250)
- ``status_code`` — the SMTP response code (``250``, ``550``, etc.), or ``None`` on connection failure
- ``status_text`` — the SMTP server's response text
- ``error`` — the exception object if a connection or protocol error occurred
- ``refused_recipients`` — a dict of recipients rejected by the server
- ``last_command`` — the last SMTP command that was attempted (``'mail'``, ``'rcpt'``, ``'data'``)


================================================
FILE: docs/requirements.txt
================================================
furo
sphinx-togglebutton


================================================
FILE: docs/transformations.rst
================================================
HTML transformer
================

.. testsetup:: *

    import emails
    import io

Message HTML body usually should be modified before sent.

Base transformations, such as css inlining can be made by `Message.transform` method:

.. doctest::

    >>> message = emails.Message(html="<style>h1{color:red}</style><h1>Hello world!</h1>")
    >>> message.transform()
    >>> message.html  # doctest: +ELLIPSIS
    '<html><head>...</head><body><h1 style="color:red">Hello world!</h1></body></html>'

`Message.transform` can take some arguments with speaken names `css_inline`, `remove_unsafe_tags`,
`make_links_absolute`, `set_content_type_meta`, `update_stylesheet`, `images_inline`.

More specific transformation can be made via `transformer` property.

Example of custom link transformations:

.. doctest::

    >>> message = emails.Message(html="<img src='promo.png'>")
    >>> message.transformer.apply_to_images(func=lambda src, **kw: 'http://mycompany.tld/images/'+src)
    >>> message.transformer.save()
    >>> message.html
    '<html><body><img src="http://mycompany.tld/images/promo.png"/></body></html>'

Example of customized making images inline:

.. doctest::

    >>> message = emails.Message(html="<img src='promo.png'>")
    >>> message.attach(filename='promo.png', data=io.BytesIO(b'PNG_DATA'))
    >>> message.attachments['promo.png'].is_inline = True
    >>> _ = message.transformer.synchronize_inline_images()
    >>> message.transformer.save()
    >>> message.html
    '<html><body><img src="cid:promo.png"/></body></html>'


Loaders
-------

python-emails ships with couple of loaders.

Load message from url:

.. code-block:: python

    import emails.loader
    message = emails.loader.from_url(url="http://xxx.github.io/newsletter/2015-08-14/index.html")


Load from zipfile or directory:

.. code-block:: python

    message = emails.loader.from_zip(open('design_pack.zip', 'rb'))
    message = emails.loader.from_directory('/home/user/design_pack')

Zipfile and directory loaders require at least one html file (with "html" extension).

Load message from `.eml` file (experimental):

.. code-block:: python

    message = emails.loader.from_rfc822(open('message.eml').read())


================================================
FILE: emails/__init__.py
================================================
"""
python-emails
~~~~~~~~~~~~~

Modern python library for email.

Build message:

   >>> import emails
   >>> message = emails.html(html="<p>Hi!<br>Here is your receipt...",
                          subject="Your receipt No. 567098123",
                          mail_from=('Some Store', 'store@somestore.com'))
   >>> message.attach(data=open('bill.pdf'), filename='bill.pdf')

send message and get response from smtp server:

   >>> r = message.send(to='s@lavr.me', smtp={'host': 'aspmx.l.google.com', 'timeout': 5})
   >>> assert r.status_code == 250

and more:

 * DKIM signature
 * Render body from template
 * Flask extension and Django integration
 * Message body transformation methods
 * Load message from url or from file


Links
`````

* `documentation <https://python-emails.readthedocs.io/>`_
* `source code <https://github.com/lavr/python-emails>`_

"""


__title__ = 'emails'
__version__ = '1.1.1'
__author__ = 'Sergey Lavrinenko'
__license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2013-2026 Sergey Lavrinenko'

USER_AGENT: str = 'python-emails/%s' % __version__

from .message import Message, html
from .utils import MessageID
from .exc import HTTPLoaderError, BadHeaderError, IncompleteMessage




================================================
FILE: emails/backend/__init__.py
================================================
from .factory import ObjectFactory
from .smtp import SMTPBackend


================================================
FILE: emails/backend/factory.py
================================================

def simple_dict2str(d):
    # Simple dict serializer
    return ";".join(["%s=%s" % (k, v) for (k, v) in d.items()])

_serializer = simple_dict2str

class ObjectFactory:

    """
    Get object from cache or create new object.
    """

    def __init__(self, cls):
        self.cls = cls
        self.pool = {}

    def __getitem__(self, k):
        if not isinstance(k, dict):
            raise ValueError("item must be dict, not %s" % type(k))
        cache_key = _serializer(k)
        obj = self.pool.get(cache_key, None)
        if obj is None:
            obj = self.cls(**k)
            self.pool[cache_key] = obj
        return obj

    def invalidate(self, k):
        cache_key = _serializer(k)
        if cache_key in self.pool:
            del self.pool[cache_key]
        return self[k]

================================================
FILE: emails/backend/inmemory/__init__.py
================================================

__all__ = ['InMemoryBackend', ]

import logging


class InMemoryBackend(object):

    """
    InMemoryBackend store message in memory for testing purposes.
    """

    def __init__(self, **kwargs):
        self.kwargs = kwargs
        self.messages = {}

    def sendmail(self, from_addr, to_addrs, msg, **kwargs):

        logging.debug('InMemoryBackend.sendmail(%s, %s, %r, %s)', from_addr, to_addrs, msg, kwargs)

        if not to_addrs:
            return None

        if not isinstance(to_addrs, (list, tuple)):
            to_addrs = [to_addrs, ]

        for addr in to_addrs:
            data = dict(from_addr=from_addr,
                        message=msg.as_string(),
                        source_message=msg,
                        **kwargs)
            self.messages.setdefault(addr.lower(), []).append(data)

        return True

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

================================================
FILE: emails/backend/response.py
================================================
from __future__ import annotations

from typing import Any


class Response:

    def __init__(self, exception: Exception | None = None, backend: Any = None) -> None:
        self.backend = backend
        self.set_exception(exception)
        self.from_addr: str | None = None
        self.to_addrs: list[str] | None = None
        self._finished: bool = False

    def set_exception(self, exc: Exception | None) -> None:
        self._exc = exc

    def raise_if_needed(self) -> None:
        if self._exc:
            raise self._exc

    @property
    def error(self) -> Exception | None:
        return self._exc

    @property
    def success(self) -> bool:
        return self._finished


class SMTPResponse(Response):

    def __init__(self, exception: Exception | None = None, backend: Any = None) -> None:

        super(SMTPResponse, self).__init__(exception=exception, backend=backend)

        self.responses: list[list] = []

        self.esmtp_opts: list[str] | None = None
        self.rcpt_options: list[str] | None = None

        self.status_code: int | None = None
        self.status_text: bytes | None = None
        self.last_command: str | None = None
        self.refused_recipients: dict[str, tuple[int, bytes]] = {}

    def set_status(self, command: str, code: int, text: bytes, **kwargs: Any) -> None:
        self.responses.append([command, code, text, kwargs])
        self.status_code = code
        self.status_text = text
        self.last_command = command

    @property
    def success(self) -> bool:
        return self._finished and self.status_code is not None and self.status_code == 250

    def __repr__(self) -> str:
        return "<emails.backend.SMTPResponse status_code=%s status_text=%s>" % (self.status_code.__repr__(),
                                                                                self.status_text.__repr__())



================================================
FILE: emails/backend/smtp/__init__.py
================================================

from .backend import SMTPBackend

try:
    from .aio_backend import AsyncSMTPBackend
except ImportError:
    pass

================================================
FILE: emails/backend/smtp/aio_backend.py
================================================
from __future__ import annotations

import asyncio
import logging
from typing import Any

import aiosmtplib

from ..response import SMTPResponse
from .aio_client import AsyncSMTPClientWithResponse
from ...utils import DNS_NAME
from .exceptions import SMTPConnectNetworkError


__all__ = ['AsyncSMTPBackend']

logger = logging.getLogger(__name__)


class AsyncSMTPBackend:

    """
    AsyncSMTPBackend manages an async SMTP connection using aiosmtplib.
    """

    DEFAULT_SOCKET_TIMEOUT = 5

    response_cls = SMTPResponse

    def __init__(self, ssl: bool = False, fail_silently: bool = True,
                 mail_options: list[str] | None = None, **kwargs: Any) -> None:

        self.ssl = ssl
        self.tls = kwargs.get('tls')
        if self.ssl and self.tls:
            raise ValueError(
                "ssl/tls are mutually exclusive, so only set "
                "one of those settings to True.")

        kwargs.setdefault('timeout', self.DEFAULT_SOCKET_TIMEOUT)
        kwargs.setdefault('local_hostname', DNS_NAME.get_fqdn())
        kwargs['port'] = int(kwargs.get('port', 0))

        self.smtp_cls_kwargs = kwargs

        self.host: str | None = kwargs.get('host')
        self.port: int = kwargs['port']
        self.fail_silently = fail_silently
        self.mail_options = mail_options or []

        self._client: AsyncSMTPClientWithResponse | None = None
        self._lock = asyncio.Lock()

    async def get_client(self) -> AsyncSMTPClientWithResponse:
        async with self._lock:
            return await self._get_client_unlocked()

    async def _get_client_unlocked(self) -> AsyncSMTPClientWithResponse:
        if self._client is None:
            client = AsyncSMTPClientWithResponse(
                parent=self, ssl=self.ssl, **self.smtp_cls_kwargs
            )
            await client.initialize()
            self._client = client
        return self._client

    async def close(self) -> None:
        """Closes the connection to the email server."""
        async with self._lock:
            await self._close_unlocked()

    async def _close_unlocked(self) -> None:
        if self._client:
            try:
                await self._client.quit()
            except Exception:
                if self.fail_silently:
                    return
                raise
            finally:
                self._client = None

    def make_response(self, exception: Exception | None = None) -> SMTPResponse:
        return self.response_cls(backend=self, exception=exception)

    async def _send(self, **kwargs: Any) -> SMTPResponse | None:
        response = None
        try:
            client = await self._get_client_unlocked()
        except aiosmtplib.SMTPConnectError as exc:
            cause = exc.__cause__
            if isinstance(cause, IOError):
                response = self.make_response(
                    exception=SMTPConnectNetworkError.from_ioerror(cause))
            else:
                response = self.make_response(exception=exc)
            if not self.fail_silently:
                raise
        except aiosmtplib.SMTPException as exc:
            response = self.make_response(exception=exc)
            if not self.fail_silently:
                raise
        except IOError as exc:
            response = self.make_response(
                exception=SMTPConnectNetworkError.from_ioerror(exc))
            if not self.fail_silently:
                raise

        if response:
            return response
        else:
            return await client.sendmail(**kwargs)

    async def _send_with_retry(self, **kwargs: Any) -> SMTPResponse | None:
        async with self._lock:
            try:
                return await self._send(**kwargs)
            except aiosmtplib.SMTPServerDisconnected:
                logger.debug('SMTPServerDisconnected, retry once')
                await self._close_unlocked()
                return await self._send(**kwargs)

    async def sendmail(self, from_addr: str, to_addrs: str | list[str],
                       msg: Any, mail_options: list[str] | None = None,
                       rcpt_options: list[str] | None = None) -> SMTPResponse | None:

        if not to_addrs:
            return None

        if not isinstance(to_addrs, (list, tuple)):
            to_addrs = [to_addrs]

        response = await self._send_with_retry(
            from_addr=from_addr,
            to_addrs=to_addrs,
            msg=msg.as_bytes(),
            mail_options=mail_options or self.mail_options,
            rcpt_options=rcpt_options,
        )

        if response and not self.fail_silently:
            response.raise_if_needed()

        return response

    async def __aenter__(self) -> AsyncSMTPBackend:
        return self

    async def __aexit__(self, exc_type: type[BaseException] | None,
                        exc_value: BaseException | None,
                        traceback: Any | None) -> None:
        await self.close()


================================================
FILE: emails/backend/smtp/aio_client.py
================================================
from __future__ import annotations

__all__ = ["AsyncSMTPClientWithResponse"]

import logging
from typing import TYPE_CHECKING

import aiosmtplib

from ..response import SMTPResponse
from ...utils import sanitize_email

if TYPE_CHECKING:
    from .aio_backend import AsyncSMTPBackend

logger = logging.getLogger(__name__)


class AsyncSMTPClientWithResponse:
    """Async SMTP client built on aiosmtplib that returns SMTPResponse objects."""

    def __init__(self, parent: AsyncSMTPBackend, **kwargs):
        self.parent = parent
        self.make_response = parent.make_response

        self.tls = kwargs.pop("tls", False)
        self.ssl = kwargs.pop("ssl", False)
        self.debug = kwargs.pop("debug", 0)
        if self.debug:
            logger.warning(
                "debug parameter is not supported in async mode; "
                "use Python logging instead"
            )
        self.user = kwargs.pop("user", None)
        self.password = kwargs.pop("password", None)

        # aiosmtplib uses use_tls for implicit TLS (SMTPS) and
        # start_tls for STARTTLS after connect
        smtp_kwargs = dict(kwargs)
        smtp_kwargs["use_tls"] = self.ssl
        smtp_kwargs["start_tls"] = self.tls

        # aiosmtplib uses 'hostname' instead of 'host'
        if "host" in smtp_kwargs:
            smtp_kwargs["hostname"] = smtp_kwargs.pop("host")

        self._smtp = aiosmtplib.SMTP(**smtp_kwargs)
        self._esmtp = False

    async def initialize(self):
        await self._smtp.connect()
        # connect() may have already completed EHLO internally
        self._esmtp = self._smtp.supports_esmtp
        try:
            if self._smtp.is_ehlo_or_helo_needed:
                try:
                    await self._smtp.ehlo()
                    self._esmtp = True
                except aiosmtplib.SMTPHeloError:
                    # aiosmtplib closes the transport on 421 responses
                    # before raising; don't attempt HELO on a dead connection
                    if not self._smtp.is_connected:
                        raise
                    await self._smtp.helo()
                    # aiosmtplib sets supports_esmtp before checking the
                    # response code, so it may be True even after EHLO
                    # failed.  Track the real state ourselves.
                    self._esmtp = False
            if self.user:
                await self._smtp.login(self.user, self.password)
        except Exception:
            await self.quit()
            raise

    async def quit(self):
        """Closes the connection to the email server."""
        try:
            await self._smtp.quit()
        except (aiosmtplib.SMTPServerDisconnected, ConnectionError):
            self._smtp.close()

    async def _rset(self):
        try:
            await self._smtp.rset()
        except (aiosmtplib.SMTPServerDisconnected, ConnectionError):
            pass

    async def sendmail(
        self,
        from_addr: str,
        to_addrs: list[str] | str,
        msg: bytes,
        mail_options: list[str] | None = None,
        rcpt_options: list[str] | None = None,
    ) -> SMTPResponse | None:

        if not to_addrs:
            return None

        rcpt_options = rcpt_options or []
        mail_options = mail_options or []
        esmtp_opts = []
        if self._esmtp:
            if self._smtp.supports_extension("size"):
                esmtp_opts.append("size=%d" % len(msg))
            for option in mail_options:
                esmtp_opts.append(option)

        response = self.make_response()

        from_addr = sanitize_email(from_addr)

        response.from_addr = from_addr
        response.esmtp_opts = esmtp_opts[:]

        try:
            resp = await self._smtp.mail(from_addr, options=esmtp_opts)
        except aiosmtplib.SMTPSenderRefused as exc:
            response.set_status(
                "mail",
                exc.code,
                exc.message.encode() if isinstance(exc.message, str) else exc.message,
            )
            response.set_exception(exc)
            await self._rset()
            return response

        response.set_status(
            "mail",
            resp.code,
            resp.message.encode() if isinstance(resp.message, str) else resp.message,
        )

        if resp.code != 250:
            await self._rset()
            response.set_exception(
                aiosmtplib.SMTPSenderRefused(resp.code, resp.message, from_addr)
            )
            return response

        if not isinstance(to_addrs, (list, tuple)):
            to_addrs = [to_addrs]

        to_addrs = [sanitize_email(e) for e in to_addrs]

        response.to_addrs = to_addrs
        response.rcpt_options = rcpt_options[:]
        response.refused_recipients = {}

        for a in to_addrs:
            try:
                resp = await self._smtp.rcpt(a, options=rcpt_options)
                code = resp.code
                resp_msg = (
                    resp.message.encode()
                    if isinstance(resp.message, str)
                    else resp.message
                )
            except aiosmtplib.SMTPRecipientRefused as exc:
                code = exc.code
                resp_msg = (
                    exc.message.encode()
                    if isinstance(exc.message, str)
                    else exc.message
                )

            response.set_status("rcpt", code, resp_msg, recipient=a)
            if (code != 250) and (code != 251):
                response.refused_recipients[a] = (code, resp_msg)

        if len(response.refused_recipients) == len(to_addrs):
            await self._rset()
            refused_list = [
                aiosmtplib.SMTPRecipientRefused(
                    code, msg.decode() if isinstance(msg, bytes) else msg, addr
                )
                for addr, (code, msg) in response.refused_recipients.items()
            ]
            response.set_exception(aiosmtplib.SMTPRecipientsRefused(refused_list))
            return response

        try:
            resp = await self._smtp.data(msg)
        except aiosmtplib.SMTPDataError as exc:
            resp_msg = (
                exc.message.encode() if isinstance(exc.message, str) else exc.message
            )
            response.set_status("data", exc.code, resp_msg)
            response.set_exception(exc)
            await self._rset()
            return response

        resp_msg = (
            resp.message.encode() if isinstance(resp.message, str) else resp.message
        )
        response.set_status("data", resp.code, resp_msg)
        if resp.code != 250:
            await self._rset()
            response.set_exception(aiosmtplib.SMTPDataError(resp.code, resp.message))
            return response

        response._finished = True
        return response


================================================
FILE: emails/backend/smtp/backend.py
================================================
from __future__ import annotations

import logging
import smtplib
from collections.abc import Callable
from functools import wraps
from types import TracebackType
from typing import Any

from ..response import SMTPResponse
from .client import SMTPClientWithResponse, SMTPClientWithResponse_SSL
from ...utils import DNS_NAME
from .exceptions import SMTPConnectNetworkError


__all__ = ['SMTPBackend']

logger = logging.getLogger(__name__)


class SMTPBackend:

    """
    SMTPBackend manages a smtp connection.
    """

    DEFAULT_SOCKET_TIMEOUT = 5

    connection_cls = SMTPClientWithResponse
    connection_ssl_cls = SMTPClientWithResponse_SSL
    response_cls = SMTPResponse

    def __init__(self, ssl: bool = False, fail_silently: bool = True,
                 mail_options: list[str] | None = None, **kwargs: Any) -> None:

        self.smtp_cls = self.connection_ssl_cls if ssl else self.connection_cls

        self.ssl = ssl
        self.tls = kwargs.get('tls')
        if self.ssl and self.tls:
            raise ValueError(
                "ssl/tls are mutually exclusive, so only set "
                "one of those settings to True.")

        kwargs.setdefault('timeout', self.DEFAULT_SOCKET_TIMEOUT)
        kwargs.setdefault('local_hostname', DNS_NAME.get_fqdn())
        kwargs['port'] = int(kwargs.get('port', 0))  # Issue #85

        self.smtp_cls_kwargs = kwargs

        self.host: str | None = kwargs.get('host')
        self.port: int = kwargs['port']  # always set as int two lines above
        self.fail_silently = fail_silently
        self.mail_options = mail_options or []

        self._client: SMTPClientWithResponse | None = None

    def get_client(self) -> SMTPClientWithResponse:
        if self._client is None:
            self._client = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)
        return self._client

    def close(self) -> None:

        """
        Closes the connection to the email server.
        """

        if self._client:
            try:
                self._client.quit()
            except Exception:
                    if self.fail_silently:
                        return
                    raise
            finally:
                self._client = None

    def make_response(self, exception: Exception | None = None) -> SMTPResponse:
        return self.response_cls(backend=self, exception=exception)

    def retry_on_disconnect(self, func: Callable[..., SMTPResponse | None]) -> Callable[..., SMTPResponse | None]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> SMTPResponse | None:
            try:
                return func(*args, **kwargs)
            except smtplib.SMTPServerDisconnected:
                # If server disconected, clear old client
                logger.debug('SMTPServerDisconnected, retry once')
                self.close()
                return func(*args, **kwargs)
        return wrapper

    def _send(self, **kwargs: Any) -> SMTPResponse | None:

        response = None
        try:
            client = self.get_client()
        except smtplib.SMTPException as exc:
            response = self.make_response(exception=exc)
            if not self.fail_silently:
                raise
        except IOError as exc:
            response = self.make_response(exception=SMTPConnectNetworkError.from_ioerror(exc))
            if not self.fail_silently:
                raise

        if response:
            return response
        else:
            return client.sendmail(**kwargs)

    def sendmail(self, from_addr: str, to_addrs: str | list[str],
                 msg: Any, mail_options: list[str] | None = None,
                 rcpt_options: list[str] | None = None) -> SMTPResponse | None:

        if not to_addrs:
            return None

        if not isinstance(to_addrs, (list, tuple)):
            to_addrs = [to_addrs, ]

        send = self.retry_on_disconnect(self._send)

        response = send(from_addr=from_addr,
                        to_addrs=to_addrs,
                        msg=msg.as_bytes(),
                        mail_options=mail_options or self.mail_options,
                        rcpt_options=rcpt_options)

        if response and not self.fail_silently:
            response.raise_if_needed()

        return response

    def __enter__(self) -> SMTPBackend:
        return self

    def __exit__(self, exc_type: type[BaseException] | None,
                 exc_value: BaseException | None,
                 traceback: TracebackType | None) -> None:
        self.close()


================================================
FILE: emails/backend/smtp/client.py
================================================
from __future__ import annotations

__all__ = ['SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']

import smtplib
from smtplib import _have_ssl, SMTP  # noqa: private API
import logging
from ..response import SMTPResponse
from ...utils import sanitize_email

logger = logging.getLogger(__name__)


class SMTPClientWithResponse(SMTP):

    def __init__(self, parent, **kwargs):

        self._initialized = False

        self.parent = parent
        self.make_response = parent.make_response
        self.tls = kwargs.pop('tls', False)
        self.ssl = kwargs.pop('ssl', False)
        self.debug = kwargs.pop('debug', 0)
        self.user = kwargs.pop('user', None)
        self.password = kwargs.pop('password', None)

        SMTP.__init__(self, **kwargs)

        try:
            self.initialize()
        except Exception:
            self.quit()
            raise


    def initialize(self):
        if not self._initialized:
            self.set_debuglevel(self.debug)
            if self.tls:
                self.starttls()
            if self.user:
                self.login(user=self.user, password=self.password)
            self.ehlo_or_helo_if_needed()
            self.initialized = True

    def quit(self):
        """Closes the connection to the email server."""
        try:
            SMTP.quit(self)
        except (smtplib.SMTPServerDisconnected, ):
            self.close()

    def _rset(self):
        try:
            self.rset()
        except smtplib.SMTPServerDisconnected:
            pass

    def sendmail(self, from_addr: str, to_addrs: list[str] | str,
                 msg: bytes, mail_options: list[str] | None = None,
                 rcpt_options: list[str] | None = None) -> SMTPResponse | None:

        if not to_addrs:
            return None

        rcpt_options = rcpt_options or []
        mail_options = mail_options or []
        esmtp_opts = []
        if self.does_esmtp:
            if self.has_extn('size'):
                esmtp_opts.append("size=%d" % len(msg))
            for option in mail_options:
                esmtp_opts.append(option)

        response = self.make_response()

        from_addr = sanitize_email(from_addr)

        response.from_addr = from_addr
        response.esmtp_opts = esmtp_opts[:]

        (code, resp) = self.mail(from_addr, esmtp_opts)
        response.set_status('mail', code, resp)

        if code != 250:
            self._rset()
            exc = smtplib.SMTPSenderRefused(code, resp, from_addr)
            response.set_exception(exc)
            return response

        if not isinstance(to_addrs, (list, tuple)):
            to_addrs = [to_addrs]

        to_addrs = [sanitize_email(e) for e in to_addrs]

        response.to_addrs = to_addrs
        response.rcpt_options = rcpt_options[:]
        response.refused_recipients = {}

        for a in to_addrs:
            (code, resp) = self.rcpt(a, rcpt_options)
            response.set_status('rcpt', code, resp, recipient=a)
            if (code != 250) and (code != 251):
                response.refused_recipients[a] = (code, resp)

        if len(response.refused_recipients) == len(to_addrs):
            # the server refused all our recipients
            self._rset()
            exc = smtplib.SMTPRecipientsRefused(response.refused_recipients)
            response.set_exception(exc)
            return response

        (code, resp) = self.data(msg)
        response.set_status('data', code, resp)
        if code != 250:
            self._rset()
            exc = smtplib.SMTPDataError(code, resp)
            response.set_exception(exc)
            return response

        response._finished = True
        return response


if _have_ssl:

    from smtplib import SMTP_SSL
    import ssl

    class SMTPClientWithResponse_SSL(SMTP_SSL, SMTPClientWithResponse):

        def __init__(self, **kw):
            args = {}
            for k in ('host', 'port', 'local_hostname', 'keyfile', 'certfile', 'timeout'):
                if k in kw:
                    args[k] = kw[k]
            SMTP_SSL.__init__(self, **args)
            SMTPClientWithResponse.__init__(self, **kw)

        def _rset(self):
            try:
                self.rset()
            except (ssl.SSLError, smtplib.SMTPServerDisconnected):
                pass

        def quit(self):
            """Closes the connection to the email server."""
            try:
                SMTPClientWithResponse.quit(self)
            except (ssl.SSLError, smtplib.SMTPServerDisconnected):
                # This happens when calling quit() on a TLS connection
                # sometimes, or when the connection was already disconnected
                # by the server.
                self.close()

        def sendmail(self, *args, **kw):
            return SMTPClientWithResponse.sendmail(self, *args, **kw)

else:

    class SMTPClientWithResponse_SSL:
        def __init__(self, *args, **kwargs):
            # should raise import error here
            import ssl





================================================
FILE: emails/backend/smtp/exceptions.py
================================================
import socket


class SMTPConnectNetworkError(IOError):
    """Network error during connection establishment."""

    @classmethod
    def from_ioerror(cls, exc):
        o = cls()
        o.errno = exc.errno
        o.filename = exc.filename
        o.strerror = exc.strerror or str(exc)
        return o


================================================
FILE: emails/django/__init__.py
================================================
from django.core.mail import get_connection
from .. message import MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage
from .. utils import sanitize_email

__all__ = ['DjangoMessageMixin', 'DjangoMessage']


class DjangoMessageMixin(object):

    _recipients = None
    _from_email = None

    @property
    def encoding(self):
        return self.charset or 'utf-8'

    def recipients(self):
        ret = self._recipients
        if ret is None:
            ret = self.get_recipients_emails()
        return [sanitize_email(e) for e in ret]

    @property
    def from_email(self):
        return sanitize_email(self._from_email or self.mail_from[1])

    def _set_emails(self, mail_to=None, set_mail_to=True, mail_from=None,
                    set_mail_from=False, to=None):

        self._recipients = None
        self._from_email = None

        mail_to = mail_to or to  # "to" is legacy

        if mail_to is not None:
            if set_mail_to:
                self.mail_to = mail_to
            else:
                self._recipients = [mail_to, ]

        if mail_from is not None:
            if set_mail_from:
                self.mail_from = mail_from
            else:
                self._from_email = mail_from

    def send(self, mail_to=None, set_mail_to=True, mail_from=None, set_mail_from=False,
             context=None, connection=None, to=None):

        self._set_emails(mail_to=mail_to, set_mail_to=set_mail_to,
                         mail_from=mail_from, set_mail_from=set_mail_from, to=to)

        if context is not None:
            self.render(**context)

        connection = connection or get_connection()
        return connection.send_messages([self, ])


class DjangoMessage(DjangoMessageMixin, MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage):
    """
    Send via django email smtp backend
    """
    pass

Message = DjangoMessage


================================================
FILE: emails/django_.py
================================================
import warnings
warnings.warn("emails.django_ module moved to emails.django", DeprecationWarning)

from .django import *

================================================
FILE: emails/exc.py
================================================
from __future__ import annotations

from dkim import DKIMException


class HTTPLoaderError(Exception):
    pass


class BadHeaderError(ValueError):
    pass


class IncompleteMessage(ValueError):
    pass

================================================
FILE: emails/loader/__init__.py
================================================
import os.path
from email.utils import formataddr

import urllib.parse as urlparse

from ..message import Message
from ..utils import fetch_url
from .local_store import (FileSystemLoader, ZipLoader, MsgLoader, FileNotFound)
from .helpers import guess_charset

class LoadError(Exception):
    pass


class IndexFileNotFound(LoadError):
    pass


class InvalidHtmlFile(LoadError):
    pass


def from_html(html, text=None, base_url=None, message_params=None, local_loader=None,
              template_cls=None, message_cls=None, source_filename=None, requests_params=None,
              **kwargs):

    """
    Loads message from html string with images from local_loader.

    :param html: html string
    :param base_url: base_url for html
    :param text: text string or None
    :param template_cls: if set, html and text are set with this template class
    :param local_loader: loader with local files
    :param message_cls: default is emails.Message
    :param message_params: parameters for Message constructor
    :param source_filename: source html file name (used for exception description on html parsing error)
    :param requests_params: parameters for external url handling
    :param kwargs: arguments for transformer.load_and_transform
    :return:
    """

    if template_cls is None:
        template_cls = lambda x: x

    message_params = message_params or {}

    _param_html = message_params.pop('html', None)
    _param_text = message_params.pop('text', None)

    message = (message_cls or Message)(html=template_cls(html or _param_html or ''),
                                       text=template_cls(text or _param_text),
                                       **message_params)
    message.create_transformer(requests_params=requests_params,
                               base_url=base_url,
                               local_loader=local_loader)
    if message.transformer.tree is None:
        raise InvalidHtmlFile("Error parsing '%s'" % source_filename)
    message.transformer.load_and_transform(**kwargs)
    message.transformer.save()
    message._loader = local_loader
    return message


from_string = from_html


def from_url(url, requests_params=None, **kwargs):

    def _extract_base_url(url):
        # /a/b.html -> /a
        p = list(urlparse.urlparse(url))[:5]
        p[2] = os.path.split(p[2])[0]
        return urlparse.urlunsplit(p)

    # Load html page
    r = fetch_url(url, requests_args=requests_params)
    html = r.content
    html = html.decode(guess_charset(r.headers, html) or 'utf-8')
    html = html.replace('\r\n', '\n')  # Remove \r

    return from_html(html,
                     base_url=_extract_base_url(url),
                     source_filename=url,
                     requests_params=requests_params,
                     **kwargs)


load_url = from_url


def _from_filebased_source(store, skip_html=False, html_filename=None, skip_text=True, text_filename=None,
                           message_params=None, **kwargs):
    """
    Loads message from prepared store `store`.

    :param store: prepared filestore
    :param skip_html: if True, make message without html part
    :param html_filename: html part filename. If None, search automatically.
    :param skip_text: if True, make message without text part
    :param text_filename: text part filename. If None, search automatically.
    :param message_params: parameters for Message
    :param kwargs: arguments for from_html
    :return:
    """

    if not skip_html:
        try:
            html_filename = store.find_index_html(html_filename)
        except FileNotFound:
            raise IndexFileNotFound('html file not found')

    dirname, html_filename = os.path.split(html_filename)
    if dirname:
        store.base_path = dirname

    html = store.content(html_filename, is_html=True, guess_charset=True)

    text = None
    if not skip_text:
        text_filename = store.find_index_text(text_filename)
        text = text_filename and store.content(text_filename) or None

    return from_html(html=html,
                     text=text,
                     local_loader=store,
                     source_filename=html_filename,
                     message_params=message_params,
                     **kwargs)


def from_directory(directory, loader_cls=None, **kwargs):
    """
    Loads message from local directory.
    Can guess for html and text part filenames (if parameters set).

    :param directory: directory path
    :param kwargs: arguments for _from_filebased_source function
    :return: emails.Message object
    """

    loader_cls = loader_cls or FileSystemLoader
    return _from_filebased_source(store=loader_cls(searchpath=directory), **kwargs)


def from_file(filename, **kwargs):
    """
    Loads message from local file.
    File `filename` must be html file.

    :param filename: filename
    :param kwargs: arguments for _from_filebased_source function
    :return: emails.Message object
    """
    return from_directory(directory=os.path.dirname(filename), html_filename=os.path.basename(filename), **kwargs)


def from_zip(zip_file, loader_cls=None, **kwargs):
    """
    Loads message from zipfile.

    :param zip_file: file-like object with zip file
    :param kwargs: arguments for _from_filebased_source function
    :return: emails.Message object
    """
    loader_cls = loader_cls or ZipLoader
    return _from_filebased_source(store=loader_cls(file=zip_file), **kwargs)


def from_rfc822(msg, loader_cls=None, message_params=None, parse_headers=False):
    # Warning: from_rfc822 is for demo purposes only
    message_params = message_params or {}
    loader_cls = loader_cls or MsgLoader

    loader = loader_cls(msg=msg)
    message = Message(html=loader.html, text=loader.text, **message_params)
    message._loader = loader

    for att in loader.attachments:
        message.attachments.add(att)

    if parse_headers:
        loader.copy_headers_to_message(message)

    return message

================================================
FILE: emails/loader/helpers.py
================================================
__all__ = ['guess_charset', 'fix_content_type']
from email.message import Message


import re
import warnings

try:
    import charade as chardet
    warnings.warn("charade module is deprecated, update your requirements to chardet",
                  DeprecationWarning)
except ImportError:
    import chardet


# HTML page charset stuff

class ReRules:
    re_meta = b"(?i)(?<=<meta).*?(?=>)"
    re_is_http_equiv = b"http-equiv=\"?'?content-type\"?'?"
    re_parse_http_equiv = b"content=\"?'?([^\"'>]+)"
    re_charset = b"charset=\"?'?([\\w-]+)\"?'?"

    def __init__(self, conv=None):
        if conv is None:
            conv = lambda x: x
        for k in dir(self):
            if k.startswith('re_'):
                setattr(self, k, re.compile(conv(getattr(self, k)), re.I + re.S + re.M))

RULES_U = ReRules(conv=lambda x: x.decode())
RULES_B = ReRules()


def guess_text_charset(text, is_html=False):
    if is_html:
        is_bytes = isinstance(text, bytes)
        rules = RULES_B if is_bytes else RULES_U
        for meta in rules.re_meta.findall(text):
            if rules.re_is_http_equiv.findall(meta):
                for content in rules.re_parse_http_equiv.findall(meta):
                    for charset in rules.re_charset.findall(content):
                        return charset.decode() if is_bytes else charset
            else:
                for charset in rules.re_charset.findall(meta):
                    return charset.decode() if is_bytes else charset
    # guess by chardet
    if isinstance(text, bytes):
        return chardet.detect(text)['encoding']


def guess_html_charset(html):
    return guess_text_charset(text=html, is_html=True)


def guess_charset(headers, html):

    # guess by http headers
    if headers:
        content_type = headers['content-type']
        if content_type:
            msg = Message()
            msg.add_header('content-type', content_type)
            r = msg.get_param('charset')
            if r:
                return r

    # guess by html content
    charset = guess_html_charset(html)
    if charset:
        return charset

COMMON_CHARSETS = ('ascii', 'utf-8', 'utf-16', 'windows-1251', 'windows-1252', 'cp850')

def decode_text(text,
                is_html=False,
                guess_charset=True,
                try_common_charsets=True,
                charsets=None,
                fallback_charset='utf-8'):

    if not isinstance(text, bytes):
        return text, None

    _charsets = []
    if guess_charset:
        c = guess_text_charset(text, is_html=is_html)
        if c:
            _charsets.append(c)

    if charsets:
        _charsets.extend(charsets)

    if try_common_charsets:
        _charsets.extend(COMMON_CHARSETS)

    if fallback_charset:
        _charsets.append(fallback_charset)

    _last_exc = None
    for enc in _charsets:
        try:
            return text.decode(enc), enc
        except UnicodeDecodeError as exc:
            _last_exc = exc

    raise _last_exc


================================================
FILE: emails/message.py
================================================
from __future__ import annotations

from collections.abc import Callable
from datetime import datetime
from email.utils import getaddresses
from typing import Any, IO

from .utils import (formataddr,
                    SafeMIMEText, SafeMIMEMultipart, sanitize_address,
                    parse_name_and_email, load_email_charsets,
                    encode_header as encode_header_,
                    renderable, format_date_header, parse_name_and_email_list,
                    cached_property, MessageID)
from .exc import BadHeaderError
from .backend import ObjectFactory, SMTPBackend
from .store import MemoryFileStore, BaseFile
from .signers import DKIMSigner


load_email_charsets()  # sic!


# Type alias for email addresses accepted by the public API
_Address = str | tuple[str | None, str] | None
_AddressList = str | tuple[str | None, str] | list[str | tuple[str | None, str]] | None


class BaseMessage:

    """
    Base email message with html part, text part and attachments.
    """

    attachment_cls = BaseFile
    filestore_cls = MemoryFileStore
    policy = None

    def __init__(self,
                 charset: str | None = None,
                 message_id: str | MessageID | bool | None = None,
                 date: str | datetime | float | bool | Callable[..., str | datetime | float] | None = None,
                 subject: str | None = None,
                 mail_from: _Address = None,
                 mail_to: _AddressList = None,
                 headers: dict[str, str] | None = None,
                 html: str | IO[str] | None = None,
                 text: str | IO[str] | None = None,
                 attachments: list[dict[str, Any] | BaseFile] | None = None,
                 cc: _AddressList = None,
                 bcc: _AddressList = None,
                 headers_encoding: str | None = None,
                 reply_to: _AddressList = None) -> None:

        self._attachments: MemoryFileStore | None = None
        self.charset: str = charset or 'utf-8'
        self.headers_encoding: str = headers_encoding or 'ascii'
        self._message_id = message_id
        self.set_subject(subject)
        self.set_date(date)
        self.set_mail_from(mail_from)
        self.set_mail_to(mail_to)
        self.set_cc(cc)
        self.set_bcc(bcc)
        self.set_reply_to(reply_to)
        self.set_headers(headers)
        self.set_html(html=html)
        self.set_text(text=text)
        self.render_data: dict[str, Any] = {}

        if attachments:
            for a in attachments:
                self.attachments.add(a)

    def set_mail_from(self, mail_from: _Address) -> None:
        # In: ('Alice', '<alice@me.com>' )
        self._mail_from = mail_from and parse_name_and_email(mail_from) or None

    def get_mail_from(self) -> tuple[str | None, str | None] | None:
        # Out: ('Alice', '<alice@me.com>') or None
        return self._mail_from

    mail_from = property(get_mail_from, set_mail_from)

    def set_mail_to(self, mail_to: _AddressList) -> None:
        self._mail_to = parse_name_and_email_list(mail_to)

    def get_mail_to(self) -> list[tuple[str | None, str | None]]:
        return self._mail_to

    mail_to = property(get_mail_to, set_mail_to)

    def set_cc(self, addr: _AddressList) -> None:
        self._cc = parse_name_and_email_list(addr)

    def get_cc(self) -> list[tuple[str | None, str | None]]:
        return self._cc

    cc = property(get_cc, set_cc)

    def set_bcc(self, addr: _AddressList) -> None:
        self._bcc = parse_name_and_email_list(addr)

    def get_bcc(self) -> list[tuple[str | None, str | None]]:
        return self._bcc

    bcc = property(get_bcc, set_bcc)

    def set_reply_to(self, addr: _AddressList) -> None:
        self._reply_to = parse_name_and_email_list(addr)

    def get_reply_to(self) -> list[tuple[str | None, str | None]]:
        return self._reply_to

    reply_to = property(get_reply_to, set_reply_to)

    def get_recipients_emails(self) -> list[str | None]:
        """
        Returns message recipient's emails for actual sending.
        :return: list of emails
        """
        return list(set([a[1] for a in self._mail_to] + [a[1] for a in self.cc] + [a[1] for a in self.bcc]))

    def set_headers(self, headers: dict[str, str] | None) -> None:
        self._headers = headers or {}

    def set_html(self, html: str | IO[str] | None, url: str | None = None) -> None:
        if hasattr(html, 'read'):
            html = html.read()
        self._html = html
        self._html_url = url

    def get_html(self) -> str | None:
        return self._html

    html = property(get_html, set_html)

    def set_text(self, text: str | IO[str] | None, url: str | None = None) -> None:
        if hasattr(text, 'read'):
            text = text.read()
        self._text = text
        self._text_url = url

    def get_text(self) -> str | None:
        return self._text

    text = property(get_text, set_text)

    @property
    @renderable
    def html_body(self) -> str | None:
        return self._html

    @property
    @renderable
    def text_body(self) -> str | None:
        return self._text

    def set_subject(self, value: str | None) -> None:
        self._subject = value

    @renderable
    def get_subject(self) -> str | None:
        return self._subject

    subject = property(get_subject, set_subject)

    def render(self, **kwargs: Any) -> None:
        self.render_data = kwargs

    def set_date(self, value: str | datetime | float | bool | Callable[..., str | datetime | float] | None) -> None:
        self._date = value

    def get_date(self) -> str | None:
        v = self._date
        if v is False:
            return None
        if callable(v):
            v = v()
        if not isinstance(v, str):
            v = format_date_header(v)
        return v

    date = property(get_date, set_date)
    message_date = date

    @property
    def message_id(self) -> str | None:
        mid = self._message_id
        if mid is False:
            return None
        return callable(mid) and mid() or mid

    @message_id.setter
    def message_id(self, value: str | MessageID | bool | None) -> None:
        self._message_id = value

    @property
    def attachments(self) -> MemoryFileStore:
        if self._attachments is None:
            self._attachments = self.filestore_cls(self.attachment_cls)
        return self._attachments

    def attach(self, **kwargs: Any) -> None:
        if 'content_disposition' not in kwargs:
            kwargs['content_disposition'] = 'attachment'
        self.attachments.add(kwargs)


class MessageBuildMixin:

    ROOT_PREAMBLE = 'This is a multi-part message in MIME format.\n'

    # Header names that contain structured address data (RFC #5322)
    ADDRESS_HEADERS = set(['from', 'sender', 'reply-to', 'to', 'cc', 'bcc',
                           'resent-from', 'resent-sender', 'resent-to',
                           'resent-cc', 'resent-bcc'])

    before_build: Callable[..., Any] | None = None
    after_build: Callable[..., Any] | None = None

    def encode_header(self, value: str | None) -> str | None:
        if value:
            return encode_header_(value, self.charset)
        else:
            return value

    def encode_address_header(self, pair: tuple[str | None, str | None] | None) -> str | None:
        if not pair:
            return None
        name, email = pair
        return formataddr((name or '', email))

    encode_name_header = encode_address_header  # legacy name

    def set_header(self, msg: SafeMIMEMultipart, key: str,
                   value: str | None, encode: bool = True) -> None:

        if value is None:
            # TODO: may be remove header here ?
            return

        if not isinstance(value, str):
            value = value.decode() if isinstance(value, bytes) else str(value)

        # Prevent header injection
        if '\n' in value or '\r' in value:
            raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (value, key))

        if key.lower() in self.ADDRESS_HEADERS:
            value = ', '.join(sanitize_address(addr, self.headers_encoding)
                              for addr in getaddresses((value,)))

        msg[key] = encode and self.encode_header(value) or value

    def _build_root_message(self, message_cls: type | None = None,
                            **kw: Any) -> SafeMIMEMultipart:

        msg = (message_cls or SafeMIMEMultipart)(**kw)

        if self.policy:
            msg.policy = self.policy

        msg.preamble = self.ROOT_PREAMBLE
        self.set_header(msg, 'Date', self.date, encode=False)
        self.set_header(msg, 'Message-ID', self.message_id, encode=False)

        if self._headers:
            for (name, value) in self._headers.items():
                self.set_header(msg, name, value)

        subject = self.subject
        if subject is not None:
            self.set_header(msg, 'Subject', subject)

        self.set_header(msg, 'From', self.encode_address_header(self.mail_from), encode=False)

        mail_to = self.mail_to
        if mail_to:
            self.set_header(msg, 'To', ", ".join([self.encode_address_header(addr) for addr in mail_to]), encode=False)

        if self.cc:
            self.set_header(msg, 'Cc', ", ".join([self.encode_address_header(addr) for addr in self.cc]), encode=False)

        if self.reply_to:
            self.set_header(msg, 'Reply-To', ", ".join([self.encode_address_header(addr) for addr in self.reply_to]), encode=False)

        return msg

    def _build_html_part(self) -> SafeMIMEText | None:
        text = self.html_body
        if text:
            p = SafeMIMEText(text, 'html', charset=self.charset)
            p.set_charset(self.charset)
            return p
        return None

    def _build_text_part(self) -> SafeMIMEText | None:
        text = self.text_body
        if text:
            p = SafeMIMEText(text, 'plain', charset=self.charset)
            p.set_charset(self.charset)
            return p
        return None

    def build_message(self, message_cls: type | None = None) -> SafeMIMEMultipart:

        if self.before_build:
            self.before_build(self)

        msg = self._build_root_message(message_cls)

        rel = SafeMIMEMultipart('related')
        msg.attach(rel)

        alt = SafeMIMEMultipart('alternative')
        rel.attach(alt)

        _text = self._build_text_part()
        _html = self._build_html_part()

        if not (_html or _text):
            raise ValueError("Message must contain 'html' or 'text'")

        if _text:
            alt.attach(_text)

        if _html:
            alt.attach(_html)

        for f in self.attachments:
            part = f.mime
            if part:
                if f.is_inline:
                    rel.attach(part)
                else:
                    msg.attach(part)

        if self.after_build:
            self.after_build(self, msg)

        return msg

    _build_message = build_message

    def as_message(self, message_cls: type | None = None) -> SafeMIMEMultipart:
        msg = self.build_message(message_cls=message_cls)
        if self._signer:
            msg = self.sign_message(msg)
        return msg

    message = as_message

    def as_string(self, message_cls: type | None = None) -> str:
        """
        Returns message as string.

        Note: this method costs one less message-to-string conversions
        for dkim in compare to self.as_message().as_string()
        """
        r = self.build_message(message_cls=message_cls).as_string()
        if self._signer:
            r = self.sign_string(r)
        return r

    def as_bytes(self, message_cls: type | None = None) -> bytes:
        """
        Returns message as bytes.
        """
        r = self.build_message(message_cls=message_cls).as_bytes()
        if self._signer:
            r = self.sign_bytes(r)
        return r


class MessageSendMixin:

    smtp_pool_factory = ObjectFactory
    smtp_cls = SMTPBackend

    @cached_property
    def smtp_pool(self) -> ObjectFactory:
        return self.smtp_pool_factory(cls=self.smtp_cls)

    def _prepare_send_params(self,
                             to: _AddressList = None,
                             set_mail_to: bool = True,
                             mail_from: _Address = None,
                             set_mail_from: bool = False,
                             render: dict[str, Any] | None = None,
                             smtp_mail_options: list[str] | None = None,
                             smtp_rcpt_options: list[str] | None = None) -> dict[str, Any]:

        if render is not None:
            self.render(**render)

        to_addrs = None

        if to:
            if set_mail_to:
                self.set_mail_to(to)
            else:
                to_addrs = [a[1] for a in parse_name_and_email_list(to)]

        to_addrs = to_addrs or self.get_recipients_emails()

        if not to_addrs:
            raise ValueError('No to-addr')

        if mail_from:
            if set_mail_from:
                self.set_mail_from(mail_from)
                from_addr = self._mail_from[1]
            else:
                mail_from = parse_name_and_email(mail_from)
                from_addr = mail_from[1]
        else:
            from_addr = self._mail_from[1]

        if not from_addr:
            raise ValueError('No "from" addr')

        return dict(from_addr=from_addr, to_addrs=to_addrs, msg=self,
                    mail_options=smtp_mail_options, rcpt_options=smtp_rcpt_options)

    def send(self,
             to: _AddressList = None,
             set_mail_to: bool = True,
             mail_from: _Address = None,
             set_mail_from: bool = False,
             render: dict[str, Any] | None = None,
             smtp_mail_options: list[str] | None = None,
             smtp_rcpt_options: list[str] | None = None,
             smtp: dict[str, Any] | SMTPBackend | None = None) -> Any:

        if smtp is None:
            smtp = {'host': 'localhost', 'port': 25, 'timeout': 5}

        if isinstance(smtp, dict):
            smtp = self.smtp_pool[smtp]

        if not hasattr(smtp, 'sendmail'):
            raise ValueError(
                "smtp must be a dict or an object with method 'sendmail'. got %s" % type(smtp))

        params = self._prepare_send_params(
            to=to, set_mail_to=set_mail_to, mail_from=mail_from,
            set_mail_from=set_mail_from, render=render,
            smtp_mail_options=smtp_mail_options, smtp_rcpt_options=smtp_rcpt_options)

        return smtp.sendmail(**params)

    async def send_async(self,
                         to: _AddressList = None,
                         set_mail_to: bool = True,
                         mail_from: _Address = None,
                         set_mail_from: bool = False,
                         render: dict[str, Any] | None = None,
                         smtp_mail_options: list[str] | None = None,
                         smtp_rcpt_options: list[str] | None = None,
                         smtp: dict[str, Any] | Any | None = None) -> Any:

        try:
            from .backend.smtp.aio_backend import AsyncSMTPBackend
        except ImportError:
            raise ImportError(
                "send_async() requires aiosmtplib. "
                'Install it with: pip install "emails[async]"') from None

        if smtp is None:
            smtp = {'host': 'localhost', 'port': 25, 'timeout': 5}

        own_backend = False
        if isinstance(smtp, dict):
            smtp = AsyncSMTPBackend(**smtp)
            own_backend = True

        if not hasattr(smtp, 'sendmail'):
            raise ValueError(
                "smtp must be a dict or an AsyncSMTPBackend. got %s" % type(smtp))

        params = self._prepare_send_params(
            to=to, set_mail_to=set_mail_to, mail_from=mail_from,
            set_mail_from=set_mail_from, render=render,
            smtp_mail_options=smtp_mail_options, smtp_rcpt_options=smtp_rcpt_options)

        try:
            return await smtp.sendmail(**params)
        finally:
            if own_backend:
                await smtp.close()


class MessageTransformerMixin:

    transformer_cls: type | None = None
    _transformer: Any = None

    def create_transformer(self, transformer_cls: type | None = None, **kw: Any) -> Any:
        cls = transformer_cls or self.transformer_cls
        if cls is None:
            from .transformer import MessageTransformer  # avoid cyclic import
            cls = MessageTransformer
        self._transformer = cls(message=self, **kw)
        return self._transformer

    def destroy_transformer(self) -> None:
        self._transformer = None

    @property
    def transformer(self) -> Any:
        if self._transformer is None:
            self.create_transformer()
        return self._transformer

    def transform(self, **kwargs: Any) -> None:
        self.transformer.load_and_transform(**kwargs)
        self.transformer.save()

    def set_html(self, **kw: Any) -> None:
        # When html set, remove old transformer
        self.destroy_transformer()
        BaseMessage.set_html(self, **kw)


class MessageSignMixin:

    signer_cls = DKIMSigner
    _signer: DKIMSigner | None = None

    def sign(self, **kwargs: Any) -> Message:
        self._signer = self.signer_cls(**kwargs)
        return self

    dkim = sign

    def sign_message(self, msg: SafeMIMEMultipart) -> SafeMIMEMultipart:
        """
        Add sign header to email.Message
        """
        return self._signer.sign_message(msg)

    def sign_string(self, message_string: str) -> str:
        """
        Add sign header to message-as-a-string
        """
        return self._signer.sign_message_string(message_string)

    def sign_bytes(self, message_bytes: bytes) -> bytes:
        """
        Add sign header to message-as-a-string
        """
        return self._signer.sign_message_bytes(message_bytes)


class Message(MessageSendMixin, MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage):
    """
    Email message with:
    - DKIM signer
    - smtp send
    - Message.transformer object
    """
    pass


def html(**kwargs: Any) -> Message:
    return Message(**kwargs)


class DjangoMessageProxy:

    """
    Class obsoletes with emails.django_.DjangoMessage

    Class looks like django.core.mail.EmailMessage for standard django email backend.

    Example usage:

        message = emails.Message(html='...', subject='...', mail_from='robot@company.ltd')
        connection = django.core.mail.get_connection()

        message.set_mail_to('somebody@somewhere.net')
        connection.send_messages([DjangoMessageProxy(message), ])
    """

    def __init__(self, message: Message, recipients: list[str] | None = None,
                 context: dict[str, Any] | None = None) -> None:
        self._message = message
        self._recipients = recipients
        self._context = context and context.copy() or {}

        self.from_email: str | None = message.mail_from[1]
        self.encoding: str = message.charset

    def recipients(self) -> list[str | None]:
        return self._recipients or [r[1] for r in self._message.mail_to]

    def message(self) -> SafeMIMEMultipart:
        self._message.render(**self._context)
        return self._message.message()


================================================
FILE: emails/py.typed
================================================


================================================
FILE: emails/signers.py
================================================
# This module uses dkimpy for DKIM signature
from __future__ import annotations

import logging
from email.mime.multipart import MIMEMultipart
from typing import IO

import dkim
from dkim import DKIMException, UnparsableKeyError


class DKIMSigner:

    def __init__(self, selector: str, domain: str, key: str | bytes | IO[bytes] | None = None,
                 ignore_sign_errors: bool = False, **kwargs: object) -> None:

        self.ignore_sign_errors = ignore_sign_errors
        self._sign_params = kwargs

        privkey = key or kwargs.pop('privkey', None)  # privkey is legacy synonym for `key`

        if not privkey:
            raise TypeError("DKIMSigner.__init__() requires 'key' argument")

        if privkey and hasattr(privkey, 'read'):
            privkey = privkey.read()

        # Normalize to bytes
        privkey_bytes = privkey if isinstance(privkey, bytes) else str(privkey).encode()

        # Validate key upfront; dkim.sign() re-parses PEM on each call
        # but the cost is negligible vs the RSA operation (~0ms vs ~2.5ms).
        try:
            dkim.crypto.parse_pem_private_key(privkey_bytes)
        except UnparsableKeyError as exc:
            raise DKIMException(exc)

        self._sign_params.update({'privkey': privkey_bytes,
                                  'domain': domain.encode(),
                                  'selector': selector.encode()})

    def get_sign_string(self, message: bytes) -> bytes | None:
        try:
            result: bytes = dkim.sign(message=message, **self._sign_params)
            return result
        except DKIMException:
            if self.ignore_sign_errors:
                logging.exception('Error signing message')
            else:
                raise
        return None

    def get_sign_bytes(self, message: bytes) -> bytes | None:
        return self.get_sign_string(message)

    def get_sign_header(self, message: bytes) -> tuple[str, str] | None:
        s = self.get_sign_string(message)
        if s:
            (header, value) = s.decode().split(': ', 1)
            if value.endswith("\r\n"):
                value = value[:-2]
            return header, value
        return None

    def sign_message(self, msg: MIMEMultipart) -> MIMEMultipart:
        """
        Add DKIM header to email.message
        """
        dkim_header = self.get_sign_header(msg.as_string().encode())
        if dkim_header:
            msg._headers.insert(0, dkim_header)  # type: ignore[attr-defined]
        return msg

    def sign_message_string(self, message_string: str) -> str:
        """
        Insert DKIM header to message string
        """
        s = self.get_sign_string(message_string.encode())
        if s:
            return s.decode() + message_string
        return message_string

    def sign_message_bytes(self, message_bytes: bytes) -> bytes:
        """
        Insert DKIM header to message bytes
        """
        s = self.get_sign_bytes(message_bytes)
        if s:
            return s + message_bytes
        return message_bytes


================================================
FILE: emails/store/__init__.py
================================================
from .store import MemoryFileStore
from .file import BaseFile, LazyHTTPFile


================================================
FILE: emails/store/file.py
================================================
from __future__ import annotations

import uuid
from mimetypes import guess_type
import puremagic
from email.mime.base import MIMEBase
from email.encoders import encode_base64
from os.path import basename
from typing import Any, IO

import urllib.parse as urlparse

from ..utils import fetch_url, encode_header


MIMETYPE_UNKNOWN = 'application/unknown'


def fix_content_type(content_type: str | None, t: str = 'image') -> str:
    if not content_type:
        return "%s/unknown" % t
    else:
        return content_type


class BaseFile:

    """
    Store base "attachment-file" information.
    """

    _data: bytes | str | IO[bytes] | None

    def __init__(self, **kwargs: Any) -> None:
        """
        uri and filename are connected properties.
        if no filename set, filename extracted from uri.
        if no uri, but filename set, then uri==filename
        """
        self.uri = kwargs.get('uri', None)
        self.absolute_url: str | None = kwargs.get('absolute_url', None) or self.uri
        self.filename = kwargs.get('filename', None)
        self.data = kwargs.get('data', None)
        self._mime_type: str | None = kwargs.get('mime_type')
        self._headers: dict[str, str] = kwargs.get('headers', {})
        self._content_id: str | None = kwargs.get('content_id')
        self._content_disposition: str | None = kwargs.get('content_disposition', 'attachment')
        self.subtype: str | None = kwargs.get('subtype')
        self.local_loader = kwargs.get('local_loader')

    def as_dict(self, fields: tuple[str, ...] | None = None) -> dict[str, Any]:
        fields = fields or ('uri', 'absolute_url', 'filename', 'data',
                            'mime_type', 'content_disposition', 'subtype')
        return dict([(k, getattr(self, k)) for k in fields])

    def get_data(self) -> bytes | str | None:
        _data = self._data
        if isinstance(_data, (str, bytes)):
            return _data
        elif _data is None:
            return None
        else:
            return _data.read()

    def set_data(self, value: bytes | str | IO[bytes] | None) -> None:
        self._data = value

    data = property(get_data, set_data)

    def get_uri(self) -> str | None:
        _uri = getattr(self, '_uri', None)
        if _uri is None:
            _filename = getattr(self, '_filename', None)
            if _filename:
                _uri = self._uri = _filename
        return _uri

    def set_uri(self, value: str | None) -> None:
        self._uri = value

    uri = property(get_uri, set_uri)

    def get_filename(self) -> str | None:
        _filename = getattr(self, '_filename', None)
        if _filename is None:
            _uri = getattr(self, '_uri', None)
            if _uri:
                parsed_path = urlparse.urlparse(_uri)
                _filename = basename(parsed_path.path)
                if not _filename:
                    _filename = str(uuid.uuid4())
                self._filename = _filename
        return _filename

    def set_filename(self, value: str | None) -> None:
        self._filename = value

    filename = property(get_filename, set_filename)

    def get_mime_type(self) -> str:
        r = getattr(self, '_mime_type', None)
        if r is None:
            filename = self.filename
            if filename:
                r = self._mime_type = guess_type(filename)[0]
        if not r:
            _data = self._data
            if isinstance(_data, bytes):
                header = _data
            elif isinstance(_data, str):
                header = _data.encode()
            elif _data is not None:
                pos = _data.tell()
                header = _data.read(128)
                _data.seek(pos)
            else:
                header = None
            if header:
                try:
                    r = puremagic.from_string(header, mime=True)
                except puremagic.PureError:
                    pass
        if not r:
            r = MIMETYPE_UNKNOWN
        self._mime_type = r
        return r

    mime_type = property(get_mime_type)

    def get_content_disposition(self) -> str | None:
        return getattr(self, '_content_disposition', None)

    def set_content_disposition(self, value: str | None) -> None:
        self._content_disposition = value

    content_disposition = property(get_content_disposition, set_content_disposition)

    @property
    def is_inline(self):
        return self.content_disposition == 'inline'

    @is_inline.setter
    def is_inline(self, value):
        if bool(value):
            self.content_disposition = 'inline'
        else:
            self.content_disposition = 'attachment'

    @property
    def content_id(self) -> str | None:
        if self._content_id is None:
            self._content_id = self.filename
        return self._content_id

    @property
    def mime(self) -> MIMEBase | None:
        content_disposition = self.content_disposition
        if content_disposition is None:
            return None
        p = getattr(self, '_cached_part', None)
        if p is None:
            filename_header = encode_header(self.filename)
            p = MIMEBase(*self.mime_type.split('/', 1), name=filename_header)
            data = self.data
            if isinstance(data, str):
                payload = data.encode()
            elif data is not None:
                payload = bytes(data)
            else:
                payload = b''
            p.set_payload(payload)
            encode_base64(p)
            if 'content-disposition' not in self._headers:
                p.add_header('Content-Disposition', self.content_disposition, filename=filename_header)
            if content_disposition == 'inline' and 'content-id' not in self._headers:
                p.add_header('Content-ID', '<%s>' % self.content_id)
            for (k, v) in self._headers.items():
                p.add_header(k, v)
            self._cached_part = p
        return p

    def reset_mime(self) -> None:
        self._mime = None

    def fetch(self) -> None:
        pass


class LazyHTTPFile(BaseFile):

    def __init__(self, requests_args: dict[str, Any] | None = None, **kwargs: Any) -> None:
        BaseFile.__init__(self, **kwargs)
        self.requests_args = requests_args
        self._fetched = False

    def fetch(self) -> None:
        if (not self._fetched) and self.uri:
            if self.local_loader:
                data = self.local_loader[self.uri]

                if data:
                    self._fetched = True
                    self._data = data
                    return

            r = fetch_url(url=self.absolute_url or self.uri, requests_args=self.requests_args)
            if r.status_code == 200:
                self._data = r.content
                self._headers = r.headers
                self._mime_type = fix_content_type(r.headers.get('content-type'), t='unknown')
                self._fetched = True

    def get_data(self) -> bytes | str:
        self.fetch()
        data = self._data
        if data is None:
            return ''
        if isinstance(data, (str, bytes)):
            return data
        return data.read()

    def set_data(self, v: bytes | str | IO[bytes] | None) -> None:
        self._data = v

    data = property(get_data, set_data)

    @property
    def mime_type(self) -> str:
        self.fetch()
        return self.get_mime_type()

    @property
    def headers(self) -> dict[str, str]:
        self.fetch()
        return self._headers


================================================
FILE: emails/store/store.py
================================================
from __future__ import annotations

from collections import OrderedDict
from collections.abc import Generator, Iterator
from os.path import splitext
from typing import Any

from .file import BaseFile


class FileStore:
    pass


class MemoryFileStore(FileStore):

    file_cls: type[BaseFile] = BaseFile

    def __init__(self, file_cls: type[BaseFile] | None = None) -> None:
        if file_cls:
            self.file_cls = file_cls
        self._files: OrderedDict[str, BaseFile] = OrderedDict()
        self._filenames: dict[str, str | None] = {}

    def __contains__(self, k: BaseFile | str | Any) -> bool:
        if isinstance(k, self.file_cls):
            return k.uri in self._files
        elif isinstance(k, str):
            return k in self._files
        else:
            return False

    def keys(self) -> list[str]:
        return list(self._files.keys())

    def __len__(self) -> int:
        return len(self._files)

    def as_dict(self) -> Generator[dict[str, Any], None, None]:
        for d in self._files.values():
            yield d.as_dict()

    def remove(self, uri: BaseFile | str) -> None:
        if isinstance(uri, self.file_cls):
            uri = uri.uri

        assert isinstance(uri, str)

        v = self[uri]
        if v:
            filename = v.filename
            if filename and (filename in self._filenames):
                del self._filenames[filename]
            del self._files[uri]

    def unique_filename(self, filename: str | None, uri: str | None = None) -> str | None:

        if filename in self._filenames:
            n = 1
            basefilename, ext = splitext(filename)

            while True:
                n += 1
                filename = "%s-%d%s" % (basefilename, n, ext)
                if filename not in self._filenames:
                    break

        if filename is not None:
            self._filenames[filename] = uri

        return filename

    def add(self, value: BaseFile | dict[str, Any], replace: bool = False) -> BaseFile:

        if isinstance(value, self.file_cls):
            uri = value.uri
        elif isinstance(value, dict):
            value = self.file_cls(**value)
            uri = value.uri
        else:
            raise ValueError("Unknown file type: %s" % type(value))

        if (uri not in self._files) or replace:
            self.remove(uri)
            value.filename = self.unique_filename(value.filename, uri=uri)
            self._files[uri] = value

        return value

    def by_uri(self, uri: str) -> BaseFile | None:
        return self._files.get(uri, None)

    def by_filename(self, filename: str) -> BaseFile | None:
        uri = self._filenames.get(filename)
        if uri:
            return self.by_uri(uri)
        return None

    def __getitem__(self, uri: str) -> BaseFile | None:
        return self.by_uri(uri) or self.by_filename(uri)

    def __iter__(self) -> Iterator[BaseFile]:
        for k in self._files:
            yield self._files[k]


================================================
FILE: emails/template/__init__.py
================================================
from .jinja_template import JinjaTemplate
from .base import StringTemplate
from .mako_template import MakoTemplate

================================================
FILE: emails/template/base.py
================================================
import string


class BaseTemplate(object):

    def __init__(self, template_text, **kwargs):
        self.set_template_text(template_text)
        self.kwargs = kwargs

    def set_template_text(self, template_text):
        self.template_text = template_text
        self._template = None

    def render(self, **kwargs):
        raise NotImplementedError

    def compile_template(self):
        raise NotImplementedError

    @property
    def template(self):
        if self._template is None:
            self._template = self.compile_template()
        return self._template


class StringTemplate(BaseTemplate):
    """
    string.Template based engine.
    """
    def compile_template(self):
        safe_substitute = self.kwargs.get('safe_substitute', True)
        t = string.Template(self.template_text)
        if safe_substitute:
            return t.safe_substitute
        else:
            return t.substitute

    def render(self, **kwargs):
        return self.template(**kwargs)

================================================
FILE: emails/template/jinja_template.py
================================================
from .base import BaseTemplate


class JinjaTemplate(BaseTemplate):
    """
    This template is mostly for demo purposes.
    You probably want to subclass from it
    and make more clear environment initialization.
    """

    DEFAULT_JINJA_ENVIRONMENT = {}

    def __init__(self, template_text, environment=None):
        super(JinjaTemplate, self).__init__(template_text)
        if environment:
            self.environment = environment
        else:
            if 'jinja2' not in globals():
                try:
                    globals()['jinja2'] = __import__('jinja2')
                except ImportError:
                    raise ImportError(
                        "jinja2 is required for template support. "
                        "Install it with: pip install emails[jinja]"
                    )
            self.environment = jinja2.Environment(**self.DEFAULT_JINJA_ENVIRONMENT)

    def compile_template(self):
        return self.environment.from_string(self.template_text)

    def render(self, **kwargs):
        return self.template.render(**kwargs)


================================================
FILE: emails/template/mako_template.py
================================================
from .base import BaseTemplate


class MakoTemplate(BaseTemplate):

    def compile_template(self):
        if 'mako_template' not in globals():
            globals()['mako_template'] = __import__('mako.template')
        return mako_template.template.Template(self.template_text)

    def render(self, **kwargs):
        return self.template.render(**kwargs)


================================================
FILE: emails/testsuite/__init__.py
================================================


================================================
FILE: emails/testsuite/conftest.py
================================================
import logging
import datetime
import pytest
import base64
import time
import random
import sys
import platform


logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()

import cssutils
cssutils.log.setLevel(logging.FATAL)


@pytest.fixture(scope='module')
def django_email_backend(request):
    from django.conf import settings
    logger.debug('django_email_backend...')
    settings.configure(EMAIL_BACKEND='django.core.mail.backends.filebased.EmailBackend',
                       EMAIL_FILE_PATH='tmp-emails')
    from django.core.mail import get_connection
    return get_connection()



================================================
FILE: emails/testsuite/django_/test_django_integrations.py
================================================
import warnings
import pytest
import emails
import emails.message

django = pytest.importorskip("django")
from emails.django import DjangoMessage

pytestmark = pytest.mark.django


def test_django_message_proxy(django_email_backend):

    """
    Send message via django email backend.
    `django_email_backend` defined in conftest.py
    """

    message_params = {'html': '<p>Test from python-emails',
                      'mail_from': 's@lavr.me',
                      'mail_to': 's.lavrinenko@gmail.com',
                      'subject': 'Test from python-emails'}
    msg = emails.html(**message_params)
    django_email_backend.send_messages([emails.message.DjangoMessageProxy(msg), ])


def test_django_message_send(django_email_backend):

    message_params = {'html': '<p>Test from python-emails',
                      'mail_from': 's@lavr.me',
                      'subject': 'Test from python-emails'}
    msg = DjangoMessage(**message_params)
    assert not msg.recipients()

    TO = 'ivan@petrov.com'
    msg.send(mail_to=TO, set_mail_to=False)
    assert msg.recipients() == [TO, ]
    assert not msg.mail_to

    TO = 'x'+TO
    msg.send(mail_to=TO)
    assert msg.recipients() == [TO, ]
    assert msg.mail_to[0][1] == TO

    msg.send(context={'a': 1})


def test_django_message_commons():

    mp = {'html': '<p>Test from python-emails',
          'mail_from': 's@lavr.me',
          'mail_to': 'jsmith@company.tld',
          'charset': 'XXX-Y'}
    msg = DjangoMessage(**mp)

    assert msg.encoding == mp['charset']

    # --- check recipients()

    assert msg.recipients() == [mp['mail_to'], ]

    msg._set_emails(mail_to='A', set_mail_to=False)
    assert msg.recipients() == ['A', ]
    assert msg.mail_to[0][1] == mp['mail_to']

    msg._set_emails(mail_to='a@a.com', set_mail_to=True)
    assert msg.recipients() == ['a@a.com', ]
    assert msg.mail_to[0][1] == 'a@a.com'

    # --- check from_email

    assert msg.from_email == mp['mail_from']

    msg._set_emails(mail_from='b@b.com', set_mail_from=False)
    assert msg.from_email == 'b@b.com'
    assert msg.mail_from[1] == mp['mail_from']

    msg._set_emails(mail_from='c@c.com', set_mail_from=True)
    assert msg.from_email == 'c@c.com'
    assert msg.mail_from[1] == 'c@c.com'


def test_legacy_import():
    """
    Test legacy django_ module exists and works
    """
    with warnings.catch_warnings(record=True) as w:
        from emails.django_ import DjangoMessage as DjangoMessageLegacy
        assert issubclass(w[-1].category, DeprecationWarning)
        assert DjangoMessageLegacy == DjangoMessage


================================================
FILE: emails/testsuite/loader/data/html_import/oldornament/oldornament/index.html
================================================
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SET-3-old-ornament</title>

</head>

<body style="background-color: #b7a98b; background-image: url(images/bg-all.jpg); color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px; text-align: left;">
	<table cellspacing="0" border="0" align="center" cellpadding="0" width="100%">
		<tr>
			<td valign="top">
				<a name="top" style="text-decoration: none; color: #cc0000;"></a>
				<table class="main-body" cellspacing="0" border="0" align="center" style="background-color: #d4c5a2; background-image: url(images/bg-main.jpg); color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px;" cellpadding="0" width="616">
					<tr>
						<td class="unsubscribe" align="center" style="padding:20px 0"> <!-- unsubscribe -->
							<p style="padding:0; margin: 0; font-family: 'Times New Roman', Times, serif; font-size: 12px;">You're receiving this newsletter because you bought widgets from us.<br />
								Having trouble reading this email? <webversion style="color: #222121; text-decoration: underline;">View it in your browser</webversion>. Not interested anymore? <unsubscribe style="color: #222121; text-decoration: underline;">Unsubscribe</unsubscribe>.</p>
							</td>
						</tr>
						<tr>
							<td class="main-td" style="padding: 0 25px;">	<!-- introduction and menu box-->
								<table class="intro" cellspacing="0" border="0" style="background-color: #e3ddca; background-image: url(images/bg-content.jpg); border-bottom: 1px solid #c3b697;" cellpadding="0" width="100%">
									<tr>
										<td valign="top" style="padding: 10px 12px 0px;" colspan="2">
											<table class="banner" cellspacing="0" border="0" style="background: #550808; color: #fcfbfa; font-family: 'Times New Roman', Times, serif;" cellpadding="0" width="100%">
												<tr>
													<td style="background: #e5ddca;"><img src="images/spacer.gif" height="2" style="display: block; border: none;" width="452" /></td>
													<td align="right" style="background: #e5ddca;"><img src="images/banner-top.gif" height="2" style="display: block; border: none;" width="90" /></td>
												</tr>
												<tr>
													<td class="title" valign="top" style="padding: 0 12px 0;">
														<img src="images/spacer.gif" width="1" height="35" style="display: block; border: none;">
														<h1 style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 60px; line-height: 60px; margin: 0;">ABC Widgets</h1>
														<p style="padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 16px; text-transform: uppercase; margin: 0;"><currentmonthname> NEWSLETTER</p>
													</td>
													<td valign="top" align="right" width="90"><img src="images/banner-middle.gif" height="144" style="display: block; border: none;" width="90" /></td>
												</tr>
											</table>
										</td>
									</tr>
									<tr>
										<td class="content" align="left" valign="top" style="font-size: 15px; font-style: italic; line-height: 18px; padding:0 35px 12px 12px; width: 329px;">
											<table width="100%" border="0" cellspacing="0" cellpadding="0" style=" color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px;">
												<tr>
													<td style="padding:25px 0 0;">
														<p style="padding:0; font-family: 'Times New Roman', Times, serif;"><strong>Dear Simon,</strong></p>
														<p style="padding:0; font-family: 'Times New Roman', Times, serif;">Welcome to lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam molestie quam vitae mi congue tristique. Aliquam lectus orci, adipiscing et, sodales ac. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien.</p>
														<p style="padding:0; font-family: 'Times New Roman', Times, serif;">Regards, ABC Widgets</p>
													</td>
												</tr>
											</table>
										</td>
										<td class="menu" align="left" valign="top" style="width: 178px; padding: 0 12px 0 0;">
											<table width="100%" border="0" cellspacing="0" cellpadding="0" style=" font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px;">
												<tr>
													<td valign="top" align="right"><img src="images/banner-bottom.png" height="55" style="display: block; border: none;" width="178" /></td>
												</tr>
												<tr>
													<td valign="top" align="left">
														<ul style="margin: 0; padding: 0;">
															<li style="font-size: 12px; font-family: 'Times New Roman', Times, serif; text-transform: uppercase; border-bottom: 1px solid #c0bcb1; color: #222121; list-style-type: none; padding: 5px 0; display:block">in this issue</li>
															<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article1" style="text-decoration: none; color: #cc0000;">Lorem ipsum dolor sit amet</a></li>
															<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article2" style="text-decoration: none; color: #cc0000;">Consectetuer adipiscing elit</a></li>
															<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article3" style="text-decoration: none; color: #cc0000;">Aliquam molestie quam vitae</a></li>
															<li style=" font-family: 'Times New Roman', Times, serif; list-style-type: none; padding: 5px 0; border-bottom: 1px solid #c0bcb1; display:block"><a href="#article4" style="text-decoration: none; color: #cc0000;">Congue tristique</a></li>
														</ul>
													</td>
												</tr>
											</table>
										</td>
									</tr>
									<tr>
										<td class="footer" valign="top" colspan="2"><img src="images/spacer.gif" height="15" style="display: block; border: none;" width="1" /></td>
									</tr>
								</table>
							</td>
						</tr>
						<tr>
							<td class="flourish" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
						</tr>
						<tr>
							<td class="main-td" valign="top" style="padding: 0 25px;">	<!-- main content -->
								<table class="main-content" cellspacing="0" border="0" style="background-color: #ded5c1; background-image: url(images/bg-content.jpg); border-bottom: 1px solid #c3b697;  color: #222121; font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px;" cellpadding="0" width="100%">
									<tr>
										<td class="content" align="left" valign="top" style="padding: 20px 15px 0 12px;">
											<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article1" style="text-decoration: none; color: #cc0000;"></a>
											<p style="padding:0; margin:0"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="300" /></p>
											<p style="padding: 0; font-family: 'Times New Roman', Times, serif;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
										</td>
										<td class="image" valign="top" style="padding: 20px 10px 15px 0;">
											<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
												<tr>
													<td style="padding: 6px;"><img src="images/img01.jpg" height="141" style="display: block; border: none;" width="213" /></td>
												</tr>
											</table>
										</td>
									</tr>
									<tr>
										<td class="content" align="left" valign="top" style="padding: 20px 15px 0 12px;">
											<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Fermentum Quam Etur Lectus</p><a name="article2" style="text-decoration: none; color: #cc0000;"></a>
											<p style="padding:0; margin:0"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="300" /></p>
											<p style="padding: 0; font-family: 'Times New Roman', Times, serif;">Suspendisse potenti--Fusce eu ante in sapien vestibulum sagittis. Cras purus. Nunc rhoncus. Donec imperdiet, nibh sit amet pharetra placerat, tortor purus condimentum lectus, at dignissim nibh velit vitae sem. Nunc condimentum blandit tortorphasellus neque vitae purus.</p>
										</td>
										<td class="image" valign="top" style="padding: 20px 10px 15px 0;">
											<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
												<tr>
													<td style="padding: 6px;"><img src="images/img02.jpg" height="141" style="display: block; border: none;" width="213" /></td>
												</tr>
											</table>
										</td>
									</tr>
									<tr>
										<td class="content" align="left" valign="top" style="padding: 20px 15px 0 12px;">
											<p class="title" style="padding: 8px 0; font-family: 'Times New Roman', Times, serif; font-size: 18px; color: #ab1212; margin: 0;">Lorem Ipsum Dolor Sit Amet</p><a name="article3" style="text-decoration: none; color: #cc0000;"></a>
											<p style="padding:0; margin:0"><img class="divider" src="images/divider.jpg" height="5" style="display: block; border: none;" width="300" /></p>
											<p style="padding: 0; font-family: 'Times New Roman', Times, serif;">Cras purus. Nunc rhoncus. Pellentesque semper. Donec imperdiet accumsan felis. Proin eget mi. Sed at est. Aliquam lectus orci, adipiscing et, sodales ac celeste. Ut dictum velit nec est. Quisque posuere, purus sit amet malesuada blandit, sapien sapien auctor arcu</p>
										</td>
										<td class="image" valign="top" style="padding: 20px 10px 15px 0;">
											<table cellspacing="0" border="0" style="background: #f0ece2; border: 1px solid #d5d2c9;" cellpadding="0" width="100%">
												<tr>
													<td style="padding: 6px;"><img src="images/img03.jpg" height="141" style="display: block; border: none;" width="213" /></td>
												</tr>
											</table>
										</td>
									</tr>
									<tr>
										<td class="footer" align="left" valign="top" style="background: #dfd8c8; padding: 10px 0 10px 14px;" colspan="2"><a href="#top" style="text-decoration: none; font-family: 'Times New Roman', Times, serif; color: #cc0000;"><strong>Back to top</strong></a></td>
									</tr>
								</table>
							</td>
						</tr>
						<tr>
							<td class="flourish" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
						</tr>
						<tr>
							<td class="main-td" valign="top" style="padding: 0 25px;">	<!-- contact box -->
								<table class="contact" cellspacing="0" border="0" style="background-color: #ded5c1; background-image: url(images/bg-content.jpg); border-bottom: 1px solid #c3b697;  color: #222121; font-family: 'Times New Roman', Times, serif;" cellpadding="0" width="100%">
									<tr>
										<td colspan="3"><img src="images/spacer.gif" height="17" style="display: block; border: none;" width="1" /></td>
									</tr>
									<tr>
										<td class="title" align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; background: #ded7c6; padding: 10px 12px; text-transform: uppercase;" width="33%"><strong>forward this issue</strong></td>
										<td class="title" align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; background: #ded7c6; padding: 10px 12px; text-transform: uppercase;" width="33%"><strong>unsubscribe</strong></td>
										<td class="title" align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; background: #ded7c6; padding: 10px 12px; text-transform: uppercase;" width="33%"><strong>contact us</strong></td>
									</tr>
									<tr>
										<td class="content" align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;">
											<p style="margin: 0; padding: 0;">Do you know someone who might be interested in receiving this monthly newsletter?</p>
										</td>
										<td class="content" align="left" style=" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;">
											<p style="margin: 0; padding: 0;">You're receiving this newsletter because you signed up for the ABC Widget Newsletter.</p>

										</td>
										<td class="content" rowspan="2" align="left" valign="top" style=" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;">
											<p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;">123 Some Street<br />
												City, State<br />
												99999<br />
												(147) 789 7745<br />
												<a href="" style="text-decoration: none; color: #cc0000;">www.abcwidgets.com</a><br />
												<a href="mailto:info@abcwidgets.com" style="text-decoration: none; color: #cc0000;">info@abcwidgets.com</a></p>
											</td>
										</tr>
										<tr>
											<td class="content" valign="top" style="font-size: 12px; padding: 10px 12px;">
												<table cellspacing="0" border="0" cellpadding="0" width="100%">
													<tr>
														<td width="43%"><p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;"><forwardtoafriend style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>forward</strong></forwardtoafriend></p></td>
														<td align="left" width="57%"><img src="images/arrow.png" height="7" style="display: block; border: none;" width="27" /></td>
													</tr>
												</table>                    	
											</td>
											<td class="content" valign="top" style="font-size: 12px; padding: 10px 12px;">                    
												<table cellspacing="0" border="0" cellpadding="0" width="100%">
													<tr>
														<td width="58%"><p style=" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;"><unsubscribe style="text-transform: uppercase; color: #cc0000; text-decoration: none;"><strong>unsubscribe</strong></unsubscribe></p></td>
														<td align="left" width="42%"><img src="images/arrow.png" height="7" style="display: block; border: none;" width="27" /></td>
													</tr>
												</table>
											</td>
										</tr>
									</table>
								</td>
							</tr>
							<tr>
								<td class="flourish" valign="top" style="padding: 22px 25px;"><img src="images/flourish.png" height="35" style="display: block; border: none;" width="566" /></td>
							</tr>
						</table>
					</td>
				</tr>
			</table>
</body>



================================================
FILE: emails/testsuite/loader/test_helpers.py
================================================

import logging; import  cssutils; cssutils.log.setLevel(logging.FATAL)

from emails.loader.helpers import (guess_charset, guess_text_charset, decode_text, guess_html_charset, RULES_U)


def test_re_rules():
    assert RULES_U.re_is_http_equiv.findall('http-equiv="Content-Type" content="text/html; charset=UTF-8"')


def test_guess_charset():
    assert guess_charset(headers={'content-type': 'text/html; charset=utf-8'}, html='') == 'utf-8'

    assert guess_charset(headers=None, html='<meta  charset="xxx-N"  >') == 'xxx-N'

    html = """<html><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />"""
    assert guess_charset(headers=None, html=html) == 'UTF-8'
    assert guess_text_charset(html, is_html=True) == 'UTF-8'
    assert guess_html_charset(html) == 'UTF-8'

    html = """Шла Саша по шоссе и сосала сушку"""
    assert guess_charset(headers=None, html=html.encode('utf-8')) == 'utf-8'


def test_decode_text():

    import encodings

    def norma_enc(enc):
        enc_ = encodings.normalize_encoding(enc.lower())
        enc_ = encodings._aliases.get(enc_) or enc_
        assert enc_
        return enc_

    assert decode_text('A')[0] == 'A'
    assert decode_text(b'A') == ('A', 'ascii')

    for enc in ['utf-8', 'windows-1251', 'cp866']:
        t = 'Шла Саша по шоссе и сосала сушку. В огороде бузина, в Киеве дядька.'
        text, guessed_encoding = decode_text(t.encode(enc))
        print(text, norma_enc(guessed_encoding))
        assert (text, norma_enc(guessed_encoding)) == (t, norma_enc(enc))

        html = """<html><meta http-equiv="Content-Type" content="text/html; charset=%s" />""" % enc
        text, guessed_encoding = decode_text(html.encode('utf-8'), is_html=True)
        print(text, norma_enc(guessed_encoding))
        assert (text, norma_enc(guessed_encoding)) == (html, norma_enc(enc))


================================================
FILE: emails/testsuite/loader/test_loaders.py
================================================
import os
from lxml.etree import XMLSyntaxError
import pytest
from requests import ConnectionError, Timeout

import emails
import emails.loader
import emails.transformer
from emails.loader.local_store import (MsgLoader, FileSystemLoader, FileNotFound, ZipLoader,
                                       split_template_path, BaseLoader)
from emails.loader.helpers import guess_charset
from emails.exc import HTTPLoaderError

ROOT = os.path.dirname(__file__)

BASE_URL = 'http://lavr.github.io/python-emails/tests/'

OLDORNAMENT_URLS = dict(from_url='campaignmonitor-samples/oldornament/index.html',
                        from_file='data/html_import/oldornament/oldornament/index.html',
                        from_zip='data/html_import/oldornament/oldornament.zip')

def test__from_html():

    with pytest.raises(Exception):
        emails.loader.from_html(html='')

    assert '-X-' in emails.loader.from_html(html='-X-').html

    # TODO: more tests for from_html func


def load_messages(from_url=None, from_file=None, from_zip=None, from_directory=None, skip_text=False, **kw):
    # Ususally all loaders loads same data
    if from_url:
        print("emails.loader.from_url", BASE_URL + from_url, kw)
        yield emails.loader.from_url(BASE_URL + from_url, **kw)
    if from_file:
        print("emails.loader.from_file", os.path.join(ROOT, from_file), kw)
        yield emails.loader.from_file(os.path.join(ROOT, from_file), skip_text=skip_text, **kw)
    if from_directory:
        print("emails.loader.from_directory", os.path.join(ROOT, from_directory), kw)
        yield emails.loader.from_directory(os.path.join(ROOT, from_directory), skip_text=skip_text, **kw)
    if from_zip:
        print("emails.loader.from_zip", os.path.join(ROOT, from_zip), kw)
        yield emails.loader.from_zip(open(os.path.join(ROOT, from_zip), 'rb'), skip_text=skip_text, **kw)


def test_loaders():

    def _all_equals(seq):
        iseq = iter(seq)
        first = next(iseq)
        return all(x == first for x in iseq)

    _base_url = os.path.dirname(BASE_URL + OLDORNAMENT_URLS['from_url']) + '/'
    def _remove_base_url(src, **kw):
        if src.startswith(_base_url):
            return src[len(_base_url):]
        else:
            return src

    message_params = {'subject': 'X', 'mail_to': 'a@b.net'}

    htmls = []

    for message in load_messages(message_params=message_params, **OLDORNAMENT_URLS):
        # Check loaded images
        assert len(message.attachments.keys()) == 13

        valid_filenames = ['arrow.png', 'banner-bottom.png', 'banner-middle.gif', 'banner-top.gif', 'bg-all.jpg',
                           'bg-content.jpg', 'bg-main.jpg', 'divider.jpg', 'flourish.png', 'img01.jpg', 'img02.jpg',
                           'img03.jpg', 'spacer.gif']
        assert sorted([a.filename for a in message.attachments]) == sorted(valid_filenames)
        print(type(message.attachments))
        assert len(message.attachments.by_filename('arrow.png').data) == 484

        # Simple html content check
        assert 'Lorem Ipsum Dolor Sit Amet' in message.html

        # Simple message build check
        message.as_string()

        # Normaliz
Download .txt
gitextract_9o3m1iyk/

├── .coveragerc
├── .github/
│   └── workflows/
│       ├── python-publish.yml
│       └── tests.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── AUTHORS.rst
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.rst
├── docs/
│   ├── Makefile
│   ├── _static/
│   │   └── .gitkeep
│   ├── advanced.rst
│   ├── api.rst
│   ├── conf.py
│   ├── examples.rst
│   ├── faq.rst
│   ├── howtohelp.rst
│   ├── index.rst
│   ├── install.rst
│   ├── links.rst
│   ├── quickstart.rst
│   ├── requirements.txt
│   └── transformations.rst
├── emails/
│   ├── __init__.py
│   ├── backend/
│   │   ├── __init__.py
│   │   ├── factory.py
│   │   ├── inmemory/
│   │   │   └── __init__.py
│   │   ├── response.py
│   │   └── smtp/
│   │       ├── __init__.py
│   │       ├── aio_backend.py
│   │       ├── aio_client.py
│   │       ├── backend.py
│   │       ├── client.py
│   │       └── exceptions.py
│   ├── django/
│   │   └── __init__.py
│   ├── django_.py
│   ├── exc.py
│   ├── loader/
│   │   ├── __init__.py
│   │   └── helpers.py
│   ├── message.py
│   ├── py.typed
│   ├── signers.py
│   ├── store/
│   │   ├── __init__.py
│   │   ├── file.py
│   │   └── store.py
│   ├── template/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── jinja_template.py
│   │   └── mako_template.py
│   ├── testsuite/
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   ├── django_/
│   │   │   └── test_django_integrations.py
│   │   ├── loader/
│   │   │   ├── data/
│   │   │   │   └── html_import/
│   │   │   │       └── oldornament/
│   │   │   │           └── oldornament/
│   │   │   │               └── index.html
│   │   │   ├── test_helpers.py
│   │   │   ├── test_loaders.py
│   │   │   └── test_rfc822_loader.py
│   │   ├── message/
│   │   │   ├── __init__.py
│   │   │   ├── helpers.py
│   │   │   ├── test_dkim.py
│   │   │   ├── test_lazy_gettext.py
│   │   │   ├── test_message.py
│   │   │   ├── test_send.py
│   │   │   ├── test_send_async.py
│   │   │   ├── test_send_async_e2e.py
│   │   │   └── test_template.py
│   │   ├── smtp/
│   │   │   ├── test_aio_client.py
│   │   │   ├── test_async_smtp_backend.py
│   │   │   ├── test_factory.py
│   │   │   ├── test_smtp_backend.py
│   │   │   └── test_smtp_response.py
│   │   ├── smtp_servers.py
│   │   ├── store/
│   │   │   └── test_store.py
│   │   ├── test_templates.py
│   │   ├── test_utils.py
│   │   └── transformer/
│   │       ├── data/
│   │       │   └── premailer_load/
│   │       │       └── style.css
│   │       ├── test_parser.py
│   │       └── test_transformer.py
│   ├── transformer.py
│   └── utils.py
├── release.sh
├── requirements/
│   ├── base.txt
│   ├── tests-base.txt
│   ├── tests-django.txt
│   └── tests.txt
├── scripts/
│   └── make_rfc822.py
├── setup.cfg
├── setup.py
└── tox.ini
Download .txt
SYMBOL INDEX (437 symbols across 47 files)

FILE: emails/backend/factory.py
  function simple_dict2str (line 2) | def simple_dict2str(d):
  class ObjectFactory (line 8) | class ObjectFactory:
    method __init__ (line 14) | def __init__(self, cls):
    method __getitem__ (line 18) | def __getitem__(self, k):
    method invalidate (line 28) | def invalidate(self, k):

FILE: emails/backend/inmemory/__init__.py
  class InMemoryBackend (line 7) | class InMemoryBackend(object):
    method __init__ (line 13) | def __init__(self, **kwargs):
    method sendmail (line 17) | def sendmail(self, from_addr, to_addrs, msg, **kwargs):
    method __enter__ (line 36) | def __enter__(self):
    method __exit__ (line 39) | def __exit__(self, exc_type, exc_value, traceback):

FILE: emails/backend/response.py
  class Response (line 6) | class Response:
    method __init__ (line 8) | def __init__(self, exception: Exception | None = None, backend: Any = ...
    method set_exception (line 15) | def set_exception(self, exc: Exception | None) -> None:
    method raise_if_needed (line 18) | def raise_if_needed(self) -> None:
    method error (line 23) | def error(self) -> Exception | None:
    method success (line 27) | def success(self) -> bool:
  class SMTPResponse (line 31) | class SMTPResponse(Response):
    method __init__ (line 33) | def __init__(self, exception: Exception | None = None, backend: Any = ...
    method set_status (line 47) | def set_status(self, command: str, code: int, text: bytes, **kwargs: A...
    method success (line 54) | def success(self) -> bool:
    method __repr__ (line 57) | def __repr__(self) -> str:

FILE: emails/backend/smtp/aio_backend.py
  class AsyncSMTPBackend (line 20) | class AsyncSMTPBackend:
    method __init__ (line 30) | def __init__(self, ssl: bool = False, fail_silently: bool = True,
    method get_client (line 54) | async def get_client(self) -> AsyncSMTPClientWithResponse:
    method _get_client_unlocked (line 58) | async def _get_client_unlocked(self) -> AsyncSMTPClientWithResponse:
    method close (line 67) | async def close(self) -> None:
    method _close_unlocked (line 72) | async def _close_unlocked(self) -> None:
    method make_response (line 83) | def make_response(self, exception: Exception | None = None) -> SMTPRes...
    method _send (line 86) | async def _send(self, **kwargs: Any) -> SMTPResponse | None:
    method _send_with_retry (line 114) | async def _send_with_retry(self, **kwargs: Any) -> SMTPResponse | None:
    method sendmail (line 123) | async def sendmail(self, from_addr: str, to_addrs: str | list[str],
    method __aenter__ (line 146) | async def __aenter__(self) -> AsyncSMTPBackend:
    method __aexit__ (line 149) | async def __aexit__(self, exc_type: type[BaseException] | None,

FILE: emails/backend/smtp/aio_client.py
  class AsyncSMTPClientWithResponse (line 19) | class AsyncSMTPClientWithResponse:
    method __init__ (line 22) | def __init__(self, parent: AsyncSMTPBackend, **kwargs):
    method initialize (line 50) | async def initialize(self):
    method quit (line 75) | async def quit(self):
    method _rset (line 82) | async def _rset(self):
    method sendmail (line 88) | async def sendmail(

FILE: emails/backend/smtp/backend.py
  class SMTPBackend (line 21) | class SMTPBackend:
    method __init__ (line 33) | def __init__(self, ssl: bool = False, fail_silently: bool = True,
    method get_client (line 58) | def get_client(self) -> SMTPClientWithResponse:
    method close (line 63) | def close(self) -> None:
    method make_response (line 79) | def make_response(self, exception: Exception | None = None) -> SMTPRes...
    method retry_on_disconnect (line 82) | def retry_on_disconnect(self, func: Callable[..., SMTPResponse | None]...
    method _send (line 94) | def _send(self, **kwargs: Any) -> SMTPResponse | None:
    method sendmail (line 113) | def sendmail(self, from_addr: str, to_addrs: str | list[str],
    method __enter__ (line 136) | def __enter__(self) -> SMTPBackend:
    method __exit__ (line 139) | def __exit__(self, exc_type: type[BaseException] | None,

FILE: emails/backend/smtp/client.py
  class SMTPClientWithResponse (line 14) | class SMTPClientWithResponse(SMTP):
    method __init__ (line 16) | def __init__(self, parent, **kwargs):
    method initialize (line 37) | def initialize(self):
    method quit (line 47) | def quit(self):
    method _rset (line 54) | def _rset(self):
    method sendmail (line 60) | def sendmail(self, from_addr: str, to_addrs: list[str] | str,
  class SMTPClientWithResponse_SSL (line 131) | class SMTPClientWithResponse_SSL(SMTP_SSL, SMTPClientWithResponse):
    method __init__ (line 133) | def __init__(self, **kw):
    method _rset (line 141) | def _rset(self):
    method quit (line 147) | def quit(self):
    method sendmail (line 157) | def sendmail(self, *args, **kw):
    method __init__ (line 163) | def __init__(self, *args, **kwargs):
  class SMTPClientWithResponse_SSL (line 162) | class SMTPClientWithResponse_SSL:
    method __init__ (line 133) | def __init__(self, **kw):
    method _rset (line 141) | def _rset(self):
    method quit (line 147) | def quit(self):
    method sendmail (line 157) | def sendmail(self, *args, **kw):
    method __init__ (line 163) | def __init__(self, *args, **kwargs):

FILE: emails/backend/smtp/exceptions.py
  class SMTPConnectNetworkError (line 4) | class SMTPConnectNetworkError(IOError):
    method from_ioerror (line 8) | def from_ioerror(cls, exc):

FILE: emails/django/__init__.py
  class DjangoMessageMixin (line 8) | class DjangoMessageMixin(object):
    method encoding (line 14) | def encoding(self):
    method recipients (line 17) | def recipients(self):
    method from_email (line 24) | def from_email(self):
    method _set_emails (line 27) | def _set_emails(self, mail_to=None, set_mail_to=True, mail_from=None,
    method send (line 47) | def send(self, mail_to=None, set_mail_to=True, mail_from=None, set_mai...
  class DjangoMessage (line 60) | class DjangoMessage(DjangoMessageMixin, MessageTransformerMixin, Message...

FILE: emails/exc.py
  class HTTPLoaderError (line 6) | class HTTPLoaderError(Exception):
  class BadHeaderError (line 10) | class BadHeaderError(ValueError):
  class IncompleteMessage (line 14) | class IncompleteMessage(ValueError):

FILE: emails/loader/__init__.py
  class LoadError (line 11) | class LoadError(Exception):
  class IndexFileNotFound (line 15) | class IndexFileNotFound(LoadError):
  class InvalidHtmlFile (line 19) | class InvalidHtmlFile(LoadError):
  function from_html (line 23) | def from_html(html, text=None, base_url=None, message_params=None, local...
  function from_url (line 68) | def from_url(url, requests_params=None, **kwargs):
  function _from_filebased_source (line 92) | def _from_filebased_source(store, skip_html=False, html_filename=None, s...
  function from_directory (line 132) | def from_directory(directory, loader_cls=None, **kwargs):
  function from_file (line 146) | def from_file(filename, **kwargs):
  function from_zip (line 158) | def from_zip(zip_file, loader_cls=None, **kwargs):
  function from_rfc822 (line 170) | def from_rfc822(msg, loader_cls=None, message_params=None, parse_headers...

FILE: emails/loader/helpers.py
  class ReRules (line 18) | class ReRules:
    method __init__ (line 24) | def __init__(self, conv=None):
  function guess_text_charset (line 35) | def guess_text_charset(text, is_html=False):
  function guess_html_charset (line 52) | def guess_html_charset(html):
  function guess_charset (line 56) | def guess_charset(headers, html):
  function decode_text (line 75) | def decode_text(text,

FILE: emails/message.py
  class BaseMessage (line 28) | class BaseMessage:
    method __init__ (line 38) | def __init__(self,
    method set_mail_from (line 74) | def set_mail_from(self, mail_from: _Address) -> None:
    method get_mail_from (line 78) | def get_mail_from(self) -> tuple[str | None, str | None] | None:
    method set_mail_to (line 84) | def set_mail_to(self, mail_to: _AddressList) -> None:
    method get_mail_to (line 87) | def get_mail_to(self) -> list[tuple[str | None, str | None]]:
    method set_cc (line 92) | def set_cc(self, addr: _AddressList) -> None:
    method get_cc (line 95) | def get_cc(self) -> list[tuple[str | None, str | None]]:
    method set_bcc (line 100) | def set_bcc(self, addr: _AddressList) -> None:
    method get_bcc (line 103) | def get_bcc(self) -> list[tuple[str | None, str | None]]:
    method set_reply_to (line 108) | def set_reply_to(self, addr: _AddressList) -> None:
    method get_reply_to (line 111) | def get_reply_to(self) -> list[tuple[str | None, str | None]]:
    method get_recipients_emails (line 116) | def get_recipients_emails(self) -> list[str | None]:
    method set_headers (line 123) | def set_headers(self, headers: dict[str, str] | None) -> None:
    method set_html (line 126) | def set_html(self, html: str | IO[str] | None, url: str | None = None)...
    method get_html (line 132) | def get_html(self) -> str | None:
    method set_text (line 137) | def set_text(self, text: str | IO[str] | None, url: str | None = None)...
    method get_text (line 143) | def get_text(self) -> str | None:
    method html_body (line 150) | def html_body(self) -> str | None:
    method text_body (line 155) | def text_body(self) -> str | None:
    method set_subject (line 158) | def set_subject(self, value: str | None) -> None:
    method get_subject (line 162) | def get_subject(self) -> str | None:
    method render (line 167) | def render(self, **kwargs: Any) -> None:
    method set_date (line 170) | def set_date(self, value: str | datetime | float | bool | Callable[......
    method get_date (line 173) | def get_date(self) -> str | None:
    method message_id (line 187) | def message_id(self) -> str | None:
    method message_id (line 194) | def message_id(self, value: str | MessageID | bool | None) -> None:
    method attachments (line 198) | def attachments(self) -> MemoryFileStore:
    method attach (line 203) | def attach(self, **kwargs: Any) -> None:
  class MessageBuildMixin (line 209) | class MessageBuildMixin:
    method encode_header (line 221) | def encode_header(self, value: str | None) -> str | None:
    method encode_address_header (line 227) | def encode_address_header(self, pair: tuple[str | None, str | None] | ...
    method set_header (line 235) | def set_header(self, msg: SafeMIMEMultipart, key: str,
    method _build_root_message (line 255) | def _build_root_message(self, message_cls: type | None = None,
    method _build_html_part (line 289) | def _build_html_part(self) -> SafeMIMEText | None:
    method _build_text_part (line 297) | def _build_text_part(self) -> SafeMIMEText | None:
    method build_message (line 305) | def build_message(self, message_cls: type | None = None) -> SafeMIMEMu...
    method as_message (line 345) | def as_message(self, message_cls: type | None = None) -> SafeMIMEMulti...
    method as_string (line 353) | def as_string(self, message_cls: type | None = None) -> str:
    method as_bytes (line 365) | def as_bytes(self, message_cls: type | None = None) -> bytes:
  class MessageSendMixin (line 375) | class MessageSendMixin:
    method smtp_pool (line 381) | def smtp_pool(self) -> ObjectFactory:
    method _prepare_send_params (line 384) | def _prepare_send_params(self,
    method send (line 425) | def send(self,
    method send_async (line 452) | async def send_async(self,
  class MessageTransformerMixin (line 493) | class MessageTransformerMixin:
    method create_transformer (line 498) | def create_transformer(self, transformer_cls: type | None = None, **kw...
    method destroy_transformer (line 506) | def destroy_transformer(self) -> None:
    method transformer (line 510) | def transformer(self) -> Any:
    method transform (line 515) | def transform(self, **kwargs: Any) -> None:
    method set_html (line 519) | def set_html(self, **kw: Any) -> None:
  class MessageSignMixin (line 525) | class MessageSignMixin:
    method sign (line 530) | def sign(self, **kwargs: Any) -> Message:
    method sign_message (line 536) | def sign_message(self, msg: SafeMIMEMultipart) -> SafeMIMEMultipart:
    method sign_string (line 542) | def sign_string(self, message_string: str) -> str:
    method sign_bytes (line 548) | def sign_bytes(self, message_bytes: bytes) -> bytes:
  class Message (line 555) | class Message(MessageSendMixin, MessageTransformerMixin, MessageSignMixi...
  function html (line 565) | def html(**kwargs: Any) -> Message:
  class DjangoMessageProxy (line 569) | class DjangoMessageProxy:
    method __init__ (line 585) | def __init__(self, message: Message, recipients: list[str] | None = None,
    method recipients (line 594) | def recipients(self) -> list[str | None]:
    method message (line 597) | def message(self) -> SafeMIMEMultipart:

FILE: emails/signers.py
  class DKIMSigner (line 12) | class DKIMSigner:
    method __init__ (line 14) | def __init__(self, selector: str, domain: str, key: str | bytes | IO[b...
    method get_sign_string (line 42) | def get_sign_string(self, message: bytes) -> bytes | None:
    method get_sign_bytes (line 53) | def get_sign_bytes(self, message: bytes) -> bytes | None:
    method get_sign_header (line 56) | def get_sign_header(self, message: bytes) -> tuple[str, str] | None:
    method sign_message (line 65) | def sign_message(self, msg: MIMEMultipart) -> MIMEMultipart:
    method sign_message_string (line 74) | def sign_message_string(self, message_string: str) -> str:
    method sign_message_bytes (line 83) | def sign_message_bytes(self, message_bytes: bytes) -> bytes:

FILE: emails/store/file.py
  function fix_content_type (line 19) | def fix_content_type(content_type: str | None, t: str = 'image') -> str:
  class BaseFile (line 26) | class BaseFile:
    method __init__ (line 34) | def __init__(self, **kwargs: Any) -> None:
    method as_dict (line 51) | def as_dict(self, fields: tuple[str, ...] | None = None) -> dict[str, ...
    method get_data (line 56) | def get_data(self) -> bytes | str | None:
    method set_data (line 65) | def set_data(self, value: bytes | str | IO[bytes] | None) -> None:
    method get_uri (line 70) | def get_uri(self) -> str | None:
    method set_uri (line 78) | def set_uri(self, value: str | None) -> None:
    method get_filename (line 83) | def get_filename(self) -> str | None:
    method set_filename (line 95) | def set_filename(self, value: str | None) -> None:
    method get_mime_type (line 100) | def get_mime_type(self) -> str:
    method get_content_disposition (line 130) | def get_content_disposition(self) -> str | None:
    method set_content_disposition (line 133) | def set_content_disposition(self, value: str | None) -> None:
    method is_inline (line 139) | def is_inline(self):
    method is_inline (line 143) | def is_inline(self, value):
    method content_id (line 150) | def content_id(self) -> str | None:
    method mime (line 156) | def mime(self) -> MIMEBase | None:
    method reset_mime (line 182) | def reset_mime(self) -> None:
    method fetch (line 185) | def fetch(self) -> None:
  class LazyHTTPFile (line 189) | class LazyHTTPFile(BaseFile):
    method __init__ (line 191) | def __init__(self, requests_args: dict[str, Any] | None = None, **kwar...
    method fetch (line 196) | def fetch(self) -> None:
    method get_data (line 213) | def get_data(self) -> bytes | str:
    method set_data (line 222) | def set_data(self, v: bytes | str | IO[bytes] | None) -> None:
    method mime_type (line 228) | def mime_type(self) -> str:
    method headers (line 233) | def headers(self) -> dict[str, str]:

FILE: emails/store/store.py
  class FileStore (line 11) | class FileStore:
  class MemoryFileStore (line 15) | class MemoryFileStore(FileStore):
    method __init__ (line 19) | def __init__(self, file_cls: type[BaseFile] | None = None) -> None:
    method __contains__ (line 25) | def __contains__(self, k: BaseFile | str | Any) -> bool:
    method keys (line 33) | def keys(self) -> list[str]:
    method __len__ (line 36) | def __len__(self) -> int:
    method as_dict (line 39) | def as_dict(self) -> Generator[dict[str, Any], None, None]:
    method remove (line 43) | def remove(self, uri: BaseFile | str) -> None:
    method unique_filename (line 56) | def unique_filename(self, filename: str | None, uri: str | None = None...
    method add (line 73) | def add(self, value: BaseFile | dict[str, Any], replace: bool = False)...
    method by_uri (line 90) | def by_uri(self, uri: str) -> BaseFile | None:
    method by_filename (line 93) | def by_filename(self, filename: str) -> BaseFile | None:
    method __getitem__ (line 99) | def __getitem__(self, uri: str) -> BaseFile | None:
    method __iter__ (line 102) | def __iter__(self) -> Iterator[BaseFile]:

FILE: emails/template/base.py
  class BaseTemplate (line 4) | class BaseTemplate(object):
    method __init__ (line 6) | def __init__(self, template_text, **kwargs):
    method set_template_text (line 10) | def set_template_text(self, template_text):
    method render (line 14) | def render(self, **kwargs):
    method compile_template (line 17) | def compile_template(self):
    method template (line 21) | def template(self):
  class StringTemplate (line 27) | class StringTemplate(BaseTemplate):
    method compile_template (line 31) | def compile_template(self):
    method render (line 39) | def render(self, **kwargs):

FILE: emails/template/jinja_template.py
  class JinjaTemplate (line 4) | class JinjaTemplate(BaseTemplate):
    method __init__ (line 13) | def __init__(self, template_text, environment=None):
    method compile_template (line 28) | def compile_template(self):
    method render (line 31) | def render(self, **kwargs):

FILE: emails/template/mako_template.py
  class MakoTemplate (line 4) | class MakoTemplate(BaseTemplate):
    method compile_template (line 6) | def compile_template(self):
    method render (line 11) | def render(self, **kwargs):

FILE: emails/testsuite/conftest.py
  function django_email_backend (line 19) | def django_email_backend(request):

FILE: emails/testsuite/django_/test_django_integrations.py
  function test_django_message_proxy (line 12) | def test_django_message_proxy(django_email_backend):
  function test_django_message_send (line 27) | def test_django_message_send(django_email_backend):
  function test_django_message_commons (line 48) | def test_django_message_commons():
  function test_legacy_import (line 83) | def test_legacy_import():

FILE: emails/testsuite/loader/test_helpers.py
  function test_re_rules (line 7) | def test_re_rules():
  function test_guess_charset (line 11) | def test_guess_charset():
  function test_decode_text (line 25) | def test_decode_text():

FILE: emails/testsuite/loader/test_loaders.py
  function test__from_html (line 22) | def test__from_html():
  function load_messages (line 32) | def load_messages(from_url=None, from_file=None, from_zip=None, from_dir...
  function test_loaders (line 48) | def test_loaders():
  function test_noindex_loaders (line 92) | def test_noindex_loaders():
  function test_loaders_with_params (line 101) | def test_loaders_with_params():
  function test_loader_image_callback (line 131) | def test_loader_image_callback():
  function test_external_urls (line 157) | def test_external_urls():
  function _get_loaders (line 183) | def _get_loaders():
  function test_local_store1 (line 189) | def test_local_store1():
  function test_split_template_path (line 208) | def test_split_template_path():
  function test_base_loader (line 214) | def test_base_loader():

FILE: emails/testsuite/loader/test_rfc822_loader.py
  function _get_message (line 10) | def _get_message():
  function _compare_messages (line 22) | def _compare_messages(a, b):
  function test_rfc822_loader (line 31) | def test_rfc822_loader(**kw):
  function test_msgloader (line 38) | def test_msgloader():
  function _try_decode (line 78) | def _try_decode(s, charsets=('utf-8', 'koi8-r', 'cp1251')):
  function _check_date (line 87) | def _check_date(s):
  function _format_addr (line 97) | def _format_addr(data, one=True):
  function test_mass_msgloader (line 106) | def test_mass_msgloader():

FILE: emails/testsuite/message/helpers.py
  function common_email_data (line 11) | def common_email_data(**kw):

FILE: emails/testsuite/message/test_dkim.py
  function dkim_keys (line 14) | def dkim_keys():
  function _check_dkim (line 33) | def _check_dkim(message, pub_key):
  function test_dkim (line 41) | def test_dkim(dkim_keys):
  function test_dkim_error (line 68) | def test_dkim_error(dkim_keys):
  function test_dkim_as_bytes (line 110) | def test_dkim_as_bytes(dkim_keys):
  function test_dkim_sign_after_error (line 119) | def test_dkim_sign_after_error(dkim_keys):
  function test_dkim_sign_twice (line 135) | def test_dkim_sign_twice(dkim_keys):

FILE: emails/testsuite/message/test_lazy_gettext.py
  function lazy_string (line 6) | def lazy_string(func, string, **variables):
  function test_lazy_translated (line 11) | def test_lazy_translated():

FILE: emails/testsuite/message/test_message.py
  function test_message_types (line 17) | def test_message_types():
  function test_message_build (line 22) | def test_message_build():
  function test_date (line 38) | def test_date():
  function test_after_build (line 62) | def test_after_build():
  function test_before_build (line 78) | def test_before_build():
  function test_sanitize_header (line 90) | def test_sanitize_header():
  function test_headers_not_double_encoded (line 100) | def test_headers_not_double_encoded():
  function test_headers_ascii_encoded (line 115) | def test_headers_ascii_encoded():
  function test_message_addresses (line 132) | def test_message_addresses():
  function test_rfc6532_address (line 149) | def test_rfc6532_address():
  function test_message_policy (line 156) | def test_message_policy():
  function test_message_id (line 176) | def test_message_id():
  function test_reply_to (line 196) | def test_reply_to():
  function test_several_recipients (line 223) | def test_several_recipients():
  function test_transform (line 245) | def test_transform():

FILE: emails/testsuite/message/test_send.py
  function get_letters (line 12) | def get_letters():
  function test_send_letters (line 32) | def test_send_letters():
  function test_send_simple (line 44) | def test_send_simple():
  function test_send_with_context_manager (line 53) | def test_send_with_context_manager():

FILE: emails/testsuite/message/test_send_async.py
  function mock_smtp (line 14) | def mock_smtp():
  function test_send_async_with_dict (line 37) | async def test_send_async_with_dict(mock_smtp):
  function test_send_async_with_backend_object (line 48) | async def test_send_async_with_backend_object(mock_smtp):
  function test_send_async_with_default_smtp (line 62) | async def test_send_async_with_default_smtp(mock_smtp):
  function test_sync_send_unchanged (line 70) | def test_sync_send_unchanged():
  function test_send_async_with_render (line 85) | async def test_send_async_with_render(mock_smtp):
  function test_send_async_with_to_override (line 97) | async def test_send_async_with_to_override(mock_smtp):
  function test_send_async_invalid_smtp_type (line 111) | async def test_send_async_invalid_smtp_type():
  function test_send_async_no_from_raises (line 119) | async def test_send_async_no_from_raises():
  function test_send_async_closes_on_error (line 131) | async def test_send_async_closes_on_error(mock_smtp):

FILE: emails/testsuite/message/test_send_async_e2e.py
  function test_send_async_simple (line 22) | async def test_send_async_simple():
  function test_send_async_with_backend_object (line 33) | async def test_send_async_with_backend_object():
  function test_send_async_with_context_manager (line 48) | async def test_send_async_with_context_manager():

FILE: emails/testsuite/message/test_template.py
  function test_templates_commons (line 6) | def test_templates_commons():
  function test_render_message_with_template (line 21) | def test_render_message_with_template():

FILE: emails/testsuite/smtp/test_aio_client.py
  function _aio_resp (line 10) | def _aio_resp(code: int = 250, message: str = 'OK'):
  class FakeAsyncSMTPBackend (line 17) | class FakeAsyncSMTPBackend:
    method make_response (line 22) | def make_response(self, exception=None):
  function parent (line 27) | def parent():
  function test_sendmail_success (line 32) | async def test_sendmail_success(parent):
  function test_sendmail_empty_to_addrs (line 60) | async def test_sendmail_empty_to_addrs(parent):
  function test_sendmail_recipient_refused (line 77) | async def test_sendmail_recipient_refused(parent):
  function test_sendmail_sender_refused (line 110) | async def test_sendmail_sender_refused(parent):
  function test_ssl_and_tls_flags (line 138) | async def test_ssl_and_tls_flags(parent):
  function test_quit_handles_disconnect (line 160) | async def test_quit_handles_disconnect(parent):
  function test_initialize_with_login (line 178) | async def test_initialize_with_login(parent):
  function test_sendmail_string_to_addrs (line 196) | async def test_sendmail_string_to_addrs(parent):

FILE: emails/testsuite/smtp/test_async_smtp_backend.py
  function mock_msg (line 13) | def mock_msg():
  function mock_smtp (line 20) | def mock_smtp():
  function test_lifecycle_connect_send_close (line 43) | async def test_lifecycle_connect_send_close(mock_smtp, mock_msg):
  function test_get_client_reuses_connection (line 69) | async def test_get_client_reuses_connection(mock_smtp, mock_msg):
  function test_get_client_with_login (line 81) | async def test_get_client_with_login(mock_smtp):
  function test_reconnect_after_disconnect (line 90) | async def test_reconnect_after_disconnect(mock_smtp, mock_msg):
  function test_fail_silently_true_on_connect_error (line 120) | async def test_fail_silently_true_on_connect_error(mock_smtp, mock_msg):
  function test_fail_silently_false_raises (line 136) | async def test_fail_silently_false_raises(mock_smtp, mock_msg):
  function test_empty_to_addrs_returns_none (line 150) | async def test_empty_to_addrs_returns_none(mock_msg):
  function test_ssl_tls_mutually_exclusive (line 162) | async def test_ssl_tls_mutually_exclusive():
  function test_context_manager (line 169) | async def test_context_manager(mock_smtp, mock_msg):
  function test_close_clears_client_on_error (line 179) | async def test_close_clears_client_on_error(mock_smtp):
  function test_string_to_addrs_converted_to_list (line 192) | async def test_string_to_addrs_converted_to_list(mock_smtp, mock_msg):
  function test_mail_options_passed_through (line 206) | async def test_mail_options_passed_through(mock_smtp, mock_msg):

FILE: emails/testsuite/smtp/test_factory.py
  function test_object_factory (line 5) | def test_object_factory():

FILE: emails/testsuite/smtp/test_smtp_backend.py
  function test_send_to_unknown_host (line 16) | def test_send_to_unknown_host():
  function test_smtp_send_with_reconnect (line 28) | def test_smtp_send_with_reconnect():
  function test_smtp_init_error (line 44) | def test_smtp_init_error():
  function test_smtp_empty_sendmail (line 52) | def test_smtp_empty_sendmail():

FILE: emails/testsuite/smtp/test_smtp_response.py
  function test_smtp_response_defaults (line 4) | def test_smtp_response_defaults():
  function test_smtp_response_set_status (line 15) | def test_smtp_response_set_status():
  function test_smtp_response_success (line 24) | def test_smtp_response_success():
  function test_smtp_response_refused_recipients (line 32) | def test_smtp_response_refused_recipients():
  function test_smtp_response_exception (line 40) | def test_smtp_response_exception():

FILE: emails/testsuite/smtp_servers.py
  function as_bool (line 11) | def as_bool(value, default=False):
  function smtp_server_from_env (line 35) | def smtp_server_from_env(name='GMAIL'):
  class SMTPTestParams (line 66) | class SMTPTestParams(object):
    method __init__ (line 70) | def __init__(self, from_email=None, to_email=None, defaults=None, **kw):
    method patch_message (line 78) | def patch_message(self, message):
    method __str__ (line 100) | def __str__(self):
    method sleep (line 105) | def sleep(self):
  function get_servers (line 113) | def get_servers():

FILE: emails/testsuite/store/test_store.py
  function test_fix_content_type (line 9) | def test_fix_content_type():
  function test_lazy_http (line 14) | def test_lazy_http():
  function test_attachment_headers (line 22) | def test_attachment_headers():
  function test_store_commons (line 28) | def test_store_commons():
  function test_store_unique_name (line 38) | def test_store_unique_name():
  function test_get_data_str (line 51) | def test_get_data_str():
  function test_get_data_bytes (line 56) | def test_get_data_bytes():
  function test_get_data_filelike (line 61) | def test_get_data_filelike():
  function test_get_data_none (line 66) | def test_get_data_none():
  function test_mime_type_from_content (line 71) | def test_mime_type_from_content():
  function test_store_commons2 (line 98) | def test_store_commons2():

FILE: emails/testsuite/test_templates.py
  function test_template_cache (line 6) | def test_template_cache():
  function test_templates_basics (line 14) | def test_templates_basics():
  function test_string_template_safe_subst (line 22) | def test_string_template_safe_subst():

FILE: emails/testsuite/test_utils.py
  function test_parse_name_and_email (line 9) | def test_parse_name_and_email():
  function test_parse_name_and_list (line 21) | def test_parse_name_and_list():
  function test_header_encode (line 28) | def test_header_encode():
  function test_sanitize_address (line 34) | def test_sanitize_address():
  function test_sanitize_email (line 42) | def test_sanitize_email():
  function test_fetch_url (line 46) | def test_fetch_url():
  function test_message_id (line 52) | def test_message_id():
  function test_url_fix (line 71) | def test_url_fix():
  function test_format_date (line 77) | def test_format_date():

FILE: emails/testsuite/transformer/test_parser.py
  function test_parser_inputs (line 4) | def test_parser_inputs():
  function test_breaking_title (line 25) | def test_breaking_title():

FILE: emails/testsuite/transformer/test_transformer.py
  function test_image_apply (line 8) | def test_image_apply():
  function test_html5_transform (line 30) | def test_html5_transform():
  function test_entity_13 (line 35) | def test_entity_13():
  function test_link_apply (line 39) | def test_link_apply():
  function test_tag_attribute (line 55) | def test_tag_attribute():
  function test_data_uri_preserved (line 69) | def test_data_uri_preserved():
  function test_local_premailer (line 97) | def test_local_premailer():
  function test_add_content_type_meta (line 103) | def test_add_content_type_meta():
  function test_image_inline (line 111) | def test_image_inline():
  function test_absolute_url (line 144) | def test_absolute_url():
  function test_html_parser_with_templates (line 151) | def test_html_parser_with_templates():
  function test_template_transformer (line 162) | def test_template_transformer():

FILE: emails/transformer.py
  class LocalPremailer (line 20) | class LocalPremailer(Premailer):
    method __init__ (line 22) | def __init__(self, html, local_loader=None, attribute_name=None, **kw):
    method _load_external (line 30) | def _load_external(self, url):
  class HTMLParser (line 61) | class HTMLParser(object):
    method __init__ (line 67) | def __init__(self, html, method=None, output_method=None):
    method html (line 80) | def html(self):
    method tree (line 84) | def tree(self):
    method to_string (line 99) | def to_string(self, encoding='utf-8', **kwargs):
    method apply_to_images (line 113) | def apply_to_images(self, func, images=True, backgrounds=True, styles_...
    method apply_to_links (line 150) | def apply_to_links(self, func):
    method add_content_type_meta (line 156) | def add_content_type_meta(self, content_type="text/html", charset="utf...
    method save (line 180) | def save(self, **kwargs):
  class BaseTransformer (line 184) | class BaseTransformer(HTMLParser):
    method __init__ (line 192) | def __init__(self, html, local_loader=None,
    method get_absolute_url (line 207) | def get_absolute_url(self, url):
    method attribute_value (line 224) | def attribute_value(self, el):
    method _default_attachment_check (line 232) | def _default_attachment_check(self, el, hints):
    method _load_attachment_func (line 238) | def _load_attachment_func(self, uri, element=None, callback=None, **kw):
    method get_premailer (line 269) | def get_premailer(self, **kw):
    method premailer (line 277) | def premailer(self):
    method remove_unsafe_tags (line 282) | def remove_unsafe_tags(self):
    method load_and_transform (line 290) | def load_and_transform(self,
    method make_all_images_inline (line 338) | def make_all_images_inline(self):
    method synchronize_inline_images (line 344) | def synchronize_inline_images(self, inline_names=None, non_inline_name...
  class Transformer (line 375) | class Transformer(BaseTransformer):
  class MessageTransformer (line 379) | class MessageTransformer(BaseTransformer):
    method __init__ (line 381) | def __init__(self, message, **kw):
    method save (line 392) | def save(self):

FILE: emails/utils.py
  function formataddr (line 27) | def formataddr(pair: tuple[str | None, str]) -> str:
  function load_email_charsets (line 52) | def load_email_charsets() -> None:
  class cached_property (line 62) | class cached_property:
    method __init__ (line 69) | def __init__(self, func: Callable[..., Any]) -> None:
    method __get__ (line 73) | def __get__(self, obj: Any, cls: type | None = None) -> Any:
  class CachedDnsName (line 83) | class CachedDnsName:
    method __str__ (line 84) | def __str__(self) -> str:
    method get_fqdn (line 87) | def get_fqdn(self) -> str:
  function decode_header (line 96) | def decode_header(value: str | bytes, default: str = "utf-8", errors: st...
  class MessageID (line 109) | class MessageID:
    method __init__ (line 117) | def __init__(self, domain: str | None = None, idstring: str | int | No...
    method __call__ (line 126) | def __call__(self) -> str:
  function parse_name_and_email_list (line 137) | def parse_name_and_email_list(elements: str | tuple[str | None, str] | l...
  function parse_name_and_email (line 170) | def parse_name_and_email(obj: str | tuple[str | None, str] | list[str],
  function sanitize_email (line 188) | def sanitize_email(addr: str, encoding: str = 'ascii', parse: bool = Fal...
  function sanitize_address (line 204) | def sanitize_address(addr: str | tuple[str, str], encoding: str = 'ascii...
  class MIMEMixin (line 217) | class MIMEMixin:
    method as_string (line 218) | def as_string(self, unixfrom: bool = False, linesep: str = '\n') -> str:
    method as_bytes (line 231) | def as_bytes(self, unixfrom: bool = False, linesep: str = '\n') -> bytes:
  class SafeMIMEText (line 244) | class SafeMIMEText(MIMEMixin, MIMEText):  # type: ignore[misc]  # intent...
    method __init__ (line 245) | def __init__(self, text: str, subtype: str, charset: str) -> None:
  class SafeMIMEMultipart (line 250) | class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):  # type: ignore[misc]...
    method __init__ (line 251) | def __init__(self, _subtype: str = 'mixed', boundary: str | None = None,
  function fetch_url (line 263) | def fetch_url(url: str, valid_http_codes: tuple[int, ...] = (200, ),
  function encode_header (line 275) | def encode_header(value: str | Any, charset: str = 'utf-8') -> str | Any:
  function renderable (line 284) | def renderable(f: F) -> F:
  function format_date_header (line 298) | def format_date_header(v: datetime | float | None, localtime: bool = Tru...

FILE: scripts/make_rfc822.py
  class MakeRFC822 (line 35) | class MakeRFC822:
    method __init__ (line 36) | def __init__(self, options):
    method _headers_from_command_line (line 39) | def _headers_from_command_line(self):
    method _get_message (line 54) | def _get_message(self):
    method _send_test_email (line 83) | def _send_test_email(self, message):
    method _start_batch (line 100) | def _start_batch(self):
    method _generate_batch (line 127) | def _generate_batch(self, batch, message):
    method main (line 137) | def main(self):

FILE: setup.py
  class run_audit (line 23) | class run_audit(Command):
    method initialize_options (line 33) | def initialize_options(self):
    method finalize_options (line 36) | def finalize_options(self):
    method run (line 39) | def run(self):
  function find_version (line 60) | def find_version(*file_paths):
  function read_file (line 72) | def read_file(filename):
Condensed preview — 91 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (323K chars).
[
  {
    "path": ".coveragerc",
    "chars": 102,
    "preview": "[run]\nsource = emails\n\n[report]\nomit = \n    emails/testsuite*\n    emails/packages*\n    emails/compat*\n"
  },
  {
    "path": ".github/workflows/python-publish.yml",
    "chars": 483,
    "preview": "name: Upload Python Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permiss"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "chars": 9033,
    "preview": "name: Tests\non:\n  push:\n    branches:\n      - master\n      - '*'\n    tags:\n      - '**'\n  pull_request:\n    branches:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 497,
    "preview": "local_settings.py\nlocal_*.py\n*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\ns"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 849,
    "preview": "repos:\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n        args: ["
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 199,
    "preview": "version: 2\n\nbuild:\n  os: ubuntu-24.04\n  tools:\n    python: \"3.12\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n  inst"
  },
  {
    "path": "AUTHORS.rst",
    "chars": 382,
    "preview": "Authors\n```````\n\npython-emails is maintained by:\n\n- `@lavr <https://github.com/lavr>`_\n\nWith inputs and contributions fr"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2245,
    "preview": "# Changelog\n\n## 1.0.2\n\n### Added\n\n- Documentation build check in CI (#208)\n- ReadTheDocs configuration (`.readthedocs.ya"
  },
  {
    "path": "LICENSE",
    "chars": 589,
    "preview": "Copyright 2013-2015 Sergey Lavrinenko\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not"
  },
  {
    "path": "MANIFEST.in",
    "chars": 68,
    "preview": "include README.rst LICENSE requirements.txt\ninclude emails/py.typed\n"
  },
  {
    "path": "Makefile",
    "chars": 1110,
    "preview": "\nDOCS_PYTHON = .venv/bin/python\nDOCS_SOURCE = docs\nDOCS_BUILD = $(DOCS_SOURCE)/_build/html\nSPHINXOPTS ?=\n\n.PHONY: clean "
  },
  {
    "path": "README.rst",
    "chars": 5231,
    "preview": "python-emails\n=============\n\n.. |pypi| image:: https://img.shields.io/pypi/v/emails.svg\n   :target: https://pypi.org/pro"
  },
  {
    "path": "docs/Makefile",
    "chars": 6790,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/_static/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/advanced.rst",
    "chars": 15731,
    "preview": "Advanced Usage\n==============\n\nThis section covers advanced features and usage patterns of ``python-emails``.\n\n\nSMTP Con"
  },
  {
    "path": "docs/api.rst",
    "chars": 21575,
    "preview": "API Reference\n=============\n\nThis section documents the public API of the ``python-emails`` library.\n\n\nMessage\n-------\n\n"
  },
  {
    "path": "docs/conf.py",
    "chars": 1927,
    "preview": "import sys\nimport os\n\nsys.path.append(os.path.abspath('..'))\n\n# -- General configuration -------------------------------"
  },
  {
    "path": "docs/examples.rst",
    "chars": 2208,
    "preview": "Features\n--------\n\n-  HTML-email message abstraction\n-  Method to transform html body:\n\n   - css inlining (using peterbe"
  },
  {
    "path": "docs/faq.rst",
    "chars": 10186,
    "preview": "FAQ\n===\n\nFrequently asked questions about ``python-emails``.\n\n\nHow do I send through Gmail / Yandex / other providers?\n-"
  },
  {
    "path": "docs/howtohelp.rst",
    "chars": 444,
    "preview": "How to Help\n===========\n\nContributions are welcome! Here is how you can help:\n\n1. `Open an issue <https://github.com/lav"
  },
  {
    "path": "docs/index.rst",
    "chars": 1585,
    "preview": "python-emails\n=============\n\n.. module:: emails\n\n|pypi| |python| |license|\n\n.. |pypi| image:: https://img.shields.io/pyp"
  },
  {
    "path": "docs/install.rst",
    "chars": 515,
    "preview": "Install\n=======\n\nInstall from pypi:\n\n.. code-block:: bash\n\n    $ pip install emails\n\nThis installs the lightweight core "
  },
  {
    "path": "docs/links.rst",
    "chars": 1756,
    "preview": "See also\n========\n\nAlternatives\n------------\n\nThere are several Python libraries for sending email, each with a differen"
  },
  {
    "path": "docs/quickstart.rst",
    "chars": 9434,
    "preview": "Quickstart\n==========\n\n``python-emails`` is a library for composing and sending email messages\nin Python. It provides a "
  },
  {
    "path": "docs/requirements.txt",
    "chars": 25,
    "preview": "furo\nsphinx-togglebutton\n"
  },
  {
    "path": "docs/transformations.rst",
    "chars": 2202,
    "preview": "HTML transformer\n================\n\n.. testsetup:: *\n\n    import emails\n    import io\n\nMessage HTML body usually should b"
  },
  {
    "path": "emails/__init__.py",
    "chars": 1218,
    "preview": "\"\"\"\npython-emails\n~~~~~~~~~~~~~\n\nModern python library for email.\n\nBuild message:\n\n   >>> import emails\n   >>> message ="
  },
  {
    "path": "emails/backend/__init__.py",
    "chars": 65,
    "preview": "from .factory import ObjectFactory\nfrom .smtp import SMTPBackend\n"
  },
  {
    "path": "emails/backend/factory.py",
    "chars": 800,
    "preview": "\ndef simple_dict2str(d):\n    # Simple dict serializer\n    return \";\".join([\"%s=%s\" % (k, v) for (k, v) in d.items()])\n\n_"
  },
  {
    "path": "emails/backend/inmemory/__init__.py",
    "chars": 964,
    "preview": "\n__all__ = ['InMemoryBackend', ]\n\nimport logging\n\n\nclass InMemoryBackend(object):\n\n    \"\"\"\n    InMemoryBackend store mes"
  },
  {
    "path": "emails/backend/response.py",
    "chars": 1880,
    "preview": "from __future__ import annotations\n\nfrom typing import Any\n\n\nclass Response:\n\n    def __init__(self, exception: Exceptio"
  },
  {
    "path": "emails/backend/smtp/__init__.py",
    "chars": 114,
    "preview": "\nfrom .backend import SMTPBackend\n\ntry:\n    from .aio_backend import AsyncSMTPBackend\nexcept ImportError:\n    pass"
  },
  {
    "path": "emails/backend/smtp/aio_backend.py",
    "chars": 4967,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import Any\n\nimport aiosmtplib\n\nfrom ..resp"
  },
  {
    "path": "emails/backend/smtp/aio_client.py",
    "chars": 6855,
    "preview": "from __future__ import annotations\n\n__all__ = [\"AsyncSMTPClientWithResponse\"]\n\nimport logging\nfrom typing import TYPE_CH"
  },
  {
    "path": "emails/backend/smtp/backend.py",
    "chars": 4545,
    "preview": "from __future__ import annotations\n\nimport logging\nimport smtplib\nfrom collections.abc import Callable\nfrom functools im"
  },
  {
    "path": "emails/backend/smtp/client.py",
    "chars": 5012,
    "preview": "from __future__ import annotations\n\n__all__ = ['SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']\n\nimport smtplib\nf"
  },
  {
    "path": "emails/backend/smtp/exceptions.py",
    "chars": 306,
    "preview": "import socket\n\n\nclass SMTPConnectNetworkError(IOError):\n    \"\"\"Network error during connection establishment.\"\"\"\n\n    @c"
  },
  {
    "path": "emails/django/__init__.py",
    "chars": 1925,
    "preview": "from django.core.mail import get_connection\nfrom .. message import MessageTransformerMixin, MessageSignMixin, MessageBui"
  },
  {
    "path": "emails/django_.py",
    "chars": 120,
    "preview": "import warnings\nwarnings.warn(\"emails.django_ module moved to emails.django\", DeprecationWarning)\n\nfrom .django import *"
  },
  {
    "path": "emails/exc.py",
    "chars": 204,
    "preview": "from __future__ import annotations\n\nfrom dkim import DKIMException\n\n\nclass HTTPLoaderError(Exception):\n    pass\n\n\nclass "
  },
  {
    "path": "emails/loader/__init__.py",
    "chars": 5990,
    "preview": "import os.path\nfrom email.utils import formataddr\n\nimport urllib.parse as urlparse\n\nfrom ..message import Message\nfrom ."
  },
  {
    "path": "emails/loader/helpers.py",
    "chars": 2993,
    "preview": "__all__ = ['guess_charset', 'fix_content_type']\nfrom email.message import Message\n\n\nimport re\nimport warnings\n\ntry:\n    "
  },
  {
    "path": "emails/message.py",
    "chars": 19430,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom datetime import datetime\nfrom email.utils "
  },
  {
    "path": "emails/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "emails/signers.py",
    "chars": 3055,
    "preview": "# This module uses dkimpy for DKIM signature\nfrom __future__ import annotations\n\nimport logging\nfrom email.mime.multipar"
  },
  {
    "path": "emails/store/__init__.py",
    "chars": 76,
    "preview": "from .store import MemoryFileStore\nfrom .file import BaseFile, LazyHTTPFile\n"
  },
  {
    "path": "emails/store/file.py",
    "chars": 7520,
    "preview": "from __future__ import annotations\n\nimport uuid\nfrom mimetypes import guess_type\nimport puremagic\nfrom email.mime.base i"
  },
  {
    "path": "emails/store/store.py",
    "chars": 2996,
    "preview": "from __future__ import annotations\n\nfrom collections import OrderedDict\nfrom collections.abc import Generator, Iterator\n"
  },
  {
    "path": "emails/template/__init__.py",
    "chars": 114,
    "preview": "from .jinja_template import JinjaTemplate\nfrom .base import StringTemplate\nfrom .mako_template import MakoTemplate"
  },
  {
    "path": "emails/template/base.py",
    "chars": 999,
    "preview": "import string\n\n\nclass BaseTemplate(object):\n\n    def __init__(self, template_text, **kwargs):\n        self.set_template_"
  },
  {
    "path": "emails/template/jinja_template.py",
    "chars": 1079,
    "preview": "from .base import BaseTemplate\n\n\nclass JinjaTemplate(BaseTemplate):\n    \"\"\"\n    This template is mostly for demo purpose"
  },
  {
    "path": "emails/template/mako_template.py",
    "chars": 360,
    "preview": "from .base import BaseTemplate\n\n\nclass MakoTemplate(BaseTemplate):\n\n    def compile_template(self):\n        if 'mako_tem"
  },
  {
    "path": "emails/testsuite/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "emails/testsuite/conftest.py",
    "chars": 607,
    "preview": "import logging\nimport datetime\nimport pytest\nimport base64\nimport time\nimport random\nimport sys\nimport platform\n\n\nloggin"
  },
  {
    "path": "emails/testsuite/django_/test_django_integrations.py",
    "chars": 2601,
    "preview": "import warnings\nimport pytest\nimport emails\nimport emails.message\n\ndjango = pytest.importorskip(\"django\")\nfrom emails.dj"
  },
  {
    "path": "emails/testsuite/loader/data/html_import/oldornament/oldornament/index.html",
    "chars": 15478,
    "preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n<head>\n<meta http-equiv=\"Cont"
  },
  {
    "path": "emails/testsuite/loader/test_helpers.py",
    "chars": 1852,
    "preview": "\nimport logging; import  cssutils; cssutils.log.setLevel(logging.FATAL)\n\nfrom emails.loader.helpers import (guess_charse"
  },
  {
    "path": "emails/testsuite/loader/test_loaders.py",
    "chars": 8358,
    "preview": "import os\nfrom lxml.etree import XMLSyntaxError\nimport pytest\nfrom requests import ConnectionError, Timeout\n\nimport emai"
  },
  {
    "path": "emails/testsuite/loader/test_rfc822_loader.py",
    "chars": 4301,
    "preview": "import glob\nimport email\nimport datetime\nimport os.path\nimport emails.loader\nfrom emails.loader.local_store import MsgLo"
  },
  {
    "path": "emails/testsuite/message/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "emails/testsuite/message/helpers.py",
    "chars": 1108,
    "preview": "import os\n\nimport emails\nfrom emails.template import JinjaTemplate\n\nTO_EMAIL = os.environ.get('SMTP_TEST_MAIL_TO') or 'p"
  },
  {
    "path": "emails/testsuite/message/test_dkim.py",
    "chars": 4651,
    "preview": "import pytest\nimport emails\nfrom emails import Message\nfrom io import StringIO\n\nfrom emails.exc import DKIMException\nimp"
  },
  {
    "path": "emails/testsuite/message/test_lazy_gettext.py",
    "chars": 658,
    "preview": "import gettext\nfrom emails import Message\nfrom emails.utils import decode_header\n\n\ndef lazy_string(func, string, **varia"
  },
  {
    "path": "emails/testsuite/message/test_message.py",
    "chars": 6923,
    "preview": "import datetime\nfrom email.utils import parseaddr\nfrom dateutil.parser import parse as dateutil_parse\nimport pytest\n\nimp"
  },
  {
    "path": "emails/testsuite/message/test_send.py",
    "chars": 2352,
    "preview": "import time\nimport random\nimport pytest\nimport emails\nimport emails.loader\nfrom emails.backend.smtp import SMTPBackend\n\n"
  },
  {
    "path": "emails/testsuite/message/test_send_async.py",
    "chars": 5050,
    "preview": "from __future__ import annotations\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nimport emails\n"
  },
  {
    "path": "emails/testsuite/message/test_send_async_e2e.py",
    "chars": 2169,
    "preview": "\"\"\"\nEnd-to-end async SMTP tests.\n\nThese tests require a running SMTP server (e.g. Mailpit) and are\nskipped unless SMTP_T"
  },
  {
    "path": "emails/testsuite/message/test_template.py",
    "chars": 988,
    "preview": "# encode: utf-8\nimport emails\nfrom emails.template import JinjaTemplate, StringTemplate, MakoTemplate\n\n\ndef test_templat"
  },
  {
    "path": "emails/testsuite/smtp/test_aio_client.py",
    "chars": 8581,
    "preview": "from __future__ import annotations\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom emails.bac"
  },
  {
    "path": "emails/testsuite/smtp/test_async_smtp_backend.py",
    "chars": 7454,
    "preview": "from __future__ import annotations\n\nimport socket\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport aiosmtpl"
  },
  {
    "path": "emails/testsuite/smtp/test_factory.py",
    "chars": 659,
    "preview": "import pytest\nfrom emails.backend.factory import ObjectFactory\n\n\ndef test_object_factory():\n    class A:\n        \"\"\" Sam"
  },
  {
    "path": "emails/testsuite/smtp/test_smtp_backend.py",
    "chars": 1816,
    "preview": "\nimport socket\n\nimport pytest\n\nimport emails\nfrom emails.backend.smtp import SMTPBackend\nfrom emails.testsuite.smtp_serv"
  },
  {
    "path": "emails/testsuite/smtp/test_smtp_response.py",
    "chars": 1217,
    "preview": "from emails.backend.response import SMTPResponse\n\n\ndef test_smtp_response_defaults():\n    r = SMTPResponse()\n    assert "
  },
  {
    "path": "emails/testsuite/smtp_servers.py",
    "chars": 3459,
    "preview": "import os\nimport platform\nimport datetime\nimport random\nimport time\n\nDEFAULT_FROM = os.environ.get('SMTP_TEST_FROM_EMAIL"
  },
  {
    "path": "emails/testsuite/store/test_store.py",
    "chars": 3361,
    "preview": "from io import BytesIO\n\nimport pytest\nimport emails\nimport emails.store\nfrom emails.store.file import BaseFile, fix_cont"
  },
  {
    "path": "emails/testsuite/test_templates.py",
    "chars": 817,
    "preview": "import pytest\nfrom emails.template import MakoTemplate, StringTemplate, JinjaTemplate\nfrom emails.template.base import B"
  },
  {
    "path": "emails/testsuite/test_utils.py",
    "chars": 2776,
    "preview": "import pytest\nimport datetime\nimport time\nfrom emails.utils import (parse_name_and_email, encode_header, decode_header, "
  },
  {
    "path": "emails/testsuite/transformer/data/premailer_load/style.css",
    "chars": 19,
    "preview": "a {color: #000000;}"
  },
  {
    "path": "emails/testsuite/transformer/test_parser.py",
    "chars": 966,
    "preview": "from emails.transformer import HTMLParser\n\n\ndef test_parser_inputs():\n\n    def _cleaned_body(s):\n        for el in ('htm"
  },
  {
    "path": "emails/testsuite/transformer/test_transformer.py",
    "chars": 6023,
    "preview": "import os.path\nimport emails.loader\nfrom emails.loader.local_store import FileSystemLoader, BaseLoader\nfrom emails.templ"
  },
  {
    "path": "emails/transformer.py",
    "chars": 13599,
    "preview": "\nimport functools\nimport logging\nimport posixpath\nimport re\nimport warnings\n\nfrom cssutils import CSSParser\nfrom lxml im"
  },
  {
    "path": "emails/utils.py",
    "chars": 10615,
    "preview": "from __future__ import annotations\n\nimport os\nimport socket\nfrom time import mktime\nfrom datetime import datetime\nfrom r"
  },
  {
    "path": "release.sh",
    "chars": 1780,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# --- Check for uncommitted changes ---\nif ! git diff --quiet || ! git diff --cac"
  },
  {
    "path": "requirements/base.txt",
    "chars": 81,
    "preview": "cssutils\nlxml\nchardet\npython-dateutil\nrequests\npremailer>=2.8.3\npuremagic\ndkimpy\n"
  },
  {
    "path": "requirements/tests-base.txt",
    "chars": 89,
    "preview": "jinja2\nmako\nspeaklater\npytest\npytest-cov\npytest-asyncio\nhtml5lib\naiosmtplib\ncryptography\n"
  },
  {
    "path": "requirements/tests-django.txt",
    "chars": 52,
    "preview": "--requirement=base.txt\n--requirement=tests-base.txt\n"
  },
  {
    "path": "requirements/tests.txt",
    "chars": 59,
    "preview": "--requirement=base.txt\n--requirement=tests-base.txt\n\ndjango"
  },
  {
    "path": "scripts/make_rfc822.py",
    "chars": 7090,
    "preview": "#!/usr/bin/env python3\n\"\"\"\n\nSimple utility that imports html from url ang print generated rfc822 message to console.\n\nEx"
  },
  {
    "path": "setup.cfg",
    "chars": 1503,
    "preview": "[mypy]\npython_version = 3.10\nignore_missing_imports = true\nwarn_return_any = true\nwarn_unused_ignores = true\nexclude = e"
  },
  {
    "path": "setup.py",
    "chars": 4071,
    "preview": "\"\"\"Setup configuration for python-emails.\"\"\"\n\nimport codecs\nimport os\nimport re\nimport sys\n\n\ntry:\n    from setuptools im"
  },
  {
    "path": "tox.ini",
    "chars": 1351,
    "preview": "[tox]\nenvlist = py310, py311, py312, py313, py314, pypy, style\n\n[testenv]\npassenv = TEST_*,SMTP_TEST_*\ncommands = py.tes"
  }
]

About this extraction

This page contains the full source code of the lavr/python-emails GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 91 files (296.2 KB), approximately 75.3k tokens, and a symbol index with 437 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!