[
  {
    "path": ".coveragerc",
    "content": "[run]\nsource = emails\n\n[report]\nomit = \n    emails/testsuite*\n    emails/packages*\n    emails/compat*\n"
  },
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "name: Upload Python Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n      - name: Install build tools\n        run: pip install build\n      - name: Build package\n        run: python -m build\n      - name: Publish to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "content": "name: Tests\non:\n  push:\n    branches:\n      - master\n      - '*'\n    tags:\n      - '**'\n  pull_request:\n    branches:\n      - master\n\njobs:\n  tests:\n    name: \"unit / ${{ matrix.name }}\"\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - {name: '3.14', python: '3.14', os: ubuntu-latest, tox: py314}\n          - {name: '3.13', python: '3.13', os: ubuntu-latest, tox: py313}\n          - {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312}\n          - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}\n          - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python }}\n          cache: pip\n      - name: update pip\n        run: |\n          pip install -U wheel\n          pip install -U setuptools\n          python -m pip install -U pip\n      - run: pip install tox\n      - name: run tests\n        run: tox -e ${{ matrix.tox }} -- -m \"not e2e and not django\"\n\n  django:\n    name: \"django / ${{ matrix.django }}\"\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - {django: '4.2', tox: django42}\n          - {django: '5.2', tox: django52}\n          - {django: '6.0', tox: django60}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n          cache: pip\n      - name: update pip\n        run: |\n          pip install -U wheel\n          pip install -U setuptools\n          python -m pip install -U pip\n      - run: pip install tox\n      - name: run django tests\n        run: tox -e ${{ matrix.tox }}\n\n  docs:\n    name: \"docs\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n          cache: pip\n      - name: install dependencies\n        run: |\n          pip install -e \".[html]\"\n          pip install sphinx -r docs/requirements.txt\n      - name: build docs\n        run: sphinx-build -W -b html docs docs/_build/html\n      - name: run doctests\n        run: sphinx-build -b doctest docs docs/_build/doctest\n\n  typecheck:\n    name: \"typecheck\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n          cache: pip\n      - name: update pip\n        run: |\n          pip install -U wheel\n          pip install -U setuptools\n          python -m pip install -U pip\n      - run: pip install tox\n      - name: run mypy\n        run: tox -e typecheck\n\n  e2e:\n    name: \"e2e / ${{ matrix.name }}\"\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - {name: '3.14', python: '3.14', os: ubuntu-latest, tox: py314}\n    services:\n      mailpit:\n        image: axllent/mailpit\n        ports:\n          - 1025:1025\n          - 8025:8025\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python }}\n          cache: pip\n      - name: update pip\n        run: |\n          pip install -U wheel\n          pip install -U setuptools\n          python -m pip install -U pip\n      - run: pip install tox\n      - name: run e2e tests\n        env:\n          SMTP_TEST_SUBJECT_SUFFIX: \"github-actions sha:${{ github.sha }} run_id:${{ github.run_id }}\"\n          SMTP_TEST_MAIL_FROM: python-emails-tests@lavr.me\n          SMTP_TEST_MAIL_TO: python-emails-tests@lavr.me\n          SMTP_TEST_SETS: LOCAL\n          SMTP_TEST_LOCAL_HOST: 127.0.0.1\n          SMTP_TEST_LOCAL_PORT: 1025\n          SMTP_TEST_LOCAL_WITHOUT_TLS: true\n        run: tox -e ${{ matrix.tox }} -- -m e2e\n\n  publish_rtd:\n    name: \"publish read the docs\"\n    needs:\n      - tests\n      - django\n      - docs\n      - typecheck\n      - e2e\n    if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))\n    runs-on: ubuntu-latest\n    env:\n      RTD_API_TOKEN: ${{ secrets.RTD_API_TOKEN }}\n      RTD_PROJECT_SLUG: python-emails\n    steps:\n      - name: Trigger Read the Docs build\n        run: |\n          set -euo pipefail\n\n          : \"${RTD_API_TOKEN:?RTD_API_TOKEN secret is required}\"\n\n          api_base=\"https://app.readthedocs.org/api/v3/projects/${RTD_PROJECT_SLUG}\"\n          auth_header=\"Authorization: Token ${RTD_API_TOKEN}\"\n\n          get_version_details() {\n            local version_slug=\"$1\"\n            local response_file=\"${2:-version.json}\"\n\n            curl \\\n              --silent \\\n              --show-error \\\n              --output \"${response_file}\" \\\n              --write-out '%{http_code}' \\\n              --header \"${auth_header}\" \\\n              \"${api_base}/versions/${version_slug}/\"\n          }\n\n          wait_for_version_slug() {\n            local version_name=\"$1\"\n\n            for attempt in {1..12}; do\n              local status_code\n              local version_slug\n              status_code=\"$(\n                curl \\\n                  --silent \\\n                  --show-error \\\n                  --output versions.json \\\n                  --write-out '%{http_code}' \\\n                  --get \\\n                  --header \"${auth_header}\" \\\n                  --data-urlencode \"type=tag\" \\\n                  --data-urlencode \"verbose_name=${version_name}\" \\\n                  \"${api_base}/versions/\"\n              )\"\n\n              if [[ \"${status_code}\" == \"200\" ]]; then\n                version_slug=\"$(\n                  jq \\\n                    --raw-output \\\n                    --arg version_name \"${version_name}\" \\\n                    '.results[] | select(.verbose_name == $version_name) | .slug' \\\n                    versions.json | head -n 1\n                )\"\n\n                if [[ -n \"${version_slug}\" && \"${version_slug}\" != \"null\" ]]; then\n                  printf '%s\\n' \"${version_slug}\"\n                  return 0\n                fi\n              fi\n\n              sleep 5\n            done\n\n            echo \"Read the Docs version '${version_name}' was not found after sync.\"\n            if [[ -f versions.json ]]; then\n              cat versions.json\n            fi\n            return 1\n          }\n\n          trigger_build() {\n            local version_slug=\"$1\"\n\n            echo \"Triggering Read the Docs build for version slug '${version_slug}'.\"\n            curl \\\n              --fail-with-body \\\n              --silent \\\n              --show-error \\\n              --request POST \\\n              --header \"${auth_header}\" \\\n              \"${api_base}/versions/${version_slug}/builds/\"\n          }\n\n          if [[ \"${GITHUB_REF_TYPE}\" == \"branch\" ]]; then\n            trigger_build latest\n            exit 0\n          fi\n\n          version_name=\"${GITHUB_REF_NAME}\"\n\n          curl \\\n            --fail-with-body \\\n            --silent \\\n            --show-error \\\n            --request POST \\\n            --header \"${auth_header}\" \\\n            \"${api_base}/sync-versions/\"\n\n          version_slug=\"$(wait_for_version_slug \"${version_name}\")\"\n          status_code=\"$(get_version_details \"${version_slug}\")\"\n\n          if [[ \"${status_code}\" != \"200\" ]]; then\n            echo \"Failed to fetch Read the Docs version details for '${version_slug}'.\"\n            cat version.json\n            exit 1\n          fi\n\n          active=\"$(jq -r '.active' version.json)\"\n          hidden=\"$(jq -r '.hidden' version.json)\"\n          echo \"Read the Docs version '${version_slug}' status: active=${active}, hidden=${hidden}.\"\n\n          if [[ \"${active}\" == \"true\" && \"${hidden}\" == \"false\" ]]; then\n            trigger_build \"${version_slug}\"\n            exit 0\n          fi\n\n          echo \"Activating and unhiding Read the Docs version '${version_slug}'.\"\n          curl \\\n            --fail-with-body \\\n            --silent \\\n            --show-error \\\n            --request PATCH \\\n            --header \"${auth_header}\" \\\n            --header \"Content-Type: application/json\" \\\n            --data '{\"active\": true, \"hidden\": false}' \\\n            \"${api_base}/versions/${version_slug}/\"\n\n          status_code=\"$(get_version_details \"${version_slug}\")\"\n\n          if [[ \"${status_code}\" != \"200\" ]]; then\n            echo \"Failed to re-fetch Read the Docs version details for '${version_slug}' after PATCH.\"\n            cat version.json\n            exit 1\n          fi\n\n          active=\"$(jq -r '.active' version.json)\"\n          hidden=\"$(jq -r '.hidden' version.json)\"\n          echo \"Read the Docs version '${version_slug}' updated status: active=${active}, hidden=${hidden}.\"\n\n          if [[ \"${active}\" == \"true\" && \"${hidden}\" == \"false\" ]]; then\n            trigger_build \"${version_slug}\"\n            exit 0\n          fi\n\n          echo \"Read the Docs version '${version_slug}' is still not buildable after PATCH.\"\n          cat version.json\n          exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "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\nsdist\ndevelop-eggs\n.installed.cfg\nlib\nlib64\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\nnosetests.xml\n\n# Translations\n*.mo\n\n# Mr Developer\n.mr.developer.cfg\n.project\n.pydevproject\n.idea\n\nvenv/\n.env\n\ndocs/plans/\ndocs/_build/\n.claude/*local*\n.claude/worktrees/\n\n# CodeQL\n.codeql-db\ncodeql-results.sarif\n\n# ralphex progress logs\n.ralphex/progress/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n        args: [\"--py310-plus\"]\n  - repo: https://github.com/asottile/reorder_python_imports\n    rev: v3.16.0\n    hooks:\n      - id: reorder-python-imports\n        name: Reorder Python imports (src, tests)\n        files: \"^(?!examples/)\"\n        args: [\"--application-directories\", \"src\"]\n  - repo: https://github.com/python/black\n    rev: 26.3.1\n    hooks:\n      - id: black\n  - repo: https://github.com/pycqa/flake8\n    rev: 7.3.0\n    hooks:\n      - id: flake8\n        additional_dependencies:\n          - flake8-bugbear\n          - flake8-implicit-str-concat\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-byte-order-marker\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "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  install:\n    - requirements: docs/requirements.txt\n    - method: pip\n      path: .\n"
  },
  {
    "path": "AUTHORS.rst",
    "content": "Authors\n```````\n\npython-emails is maintained by:\n\n- `@lavr <https://github.com/lavr>`_\n\nWith inputs and contributions from (in chronological order):\n\n- `@smihai <https://github.com/smihai>`_\n- `@Daviey <https://github.com/Daviey>`_\n- `@positiveviking <https://github.com/positiveviking>`_\n\nSee `all Github contributors <https://github.com/lavr/python-emails/graphs/contributors>`_\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 1.0.2\n\n### Added\n\n- Documentation build check in CI (#208)\n- ReadTheDocs configuration (`.readthedocs.yaml`)\n\n### Fixed\n\n- Jinja2 is now an optional dependency — install with `pip install emails[jinja2]` (#207, #161)\n\n## 1.0\n\n### Breaking changes\n\n- Require Python 3.10+ (dropped 3.9) (#188)\n- HTML transformation dependencies (`cssutils`, `lxml`, `chardet`, `requests`, `premailer`) are now optional — install with `pip install emails[html]` (#190)\n- Removed Python 2 compatibility helpers `to_bytes`, `to_native`, `to_unicode` from `emails.utils` (#197)\n- Replaced vendored `emails.packages.dkim` with upstream `dkimpy` package — use `import dkim` directly (#196)\n\n### Added\n\n- `reply_to` parameter for Message (#115)\n- Content-based MIME type detection via `puremagic` when file extension is missing (#163)\n- Data URI support in transformer — `data:` URIs are preserved as-is (#62)\n- Type hints for public API (#191)\n- mypy in CI (#194)\n- Python 3.13 and 3.14 support (#184)\n- Django CI jobs with Django 4.2, 5.2, 6.0 (#201)\n- CC/BCC importing in MsgLoader (#182)\n- RFC 6532 support — non-ASCII characters in email addresses (#138)\n- In-memory SMTP backend (#136)\n- SMTP integration tests using Mailpit (#186)\n\n### Fixed\n\n- Double stream read in `BaseFile.mime` for file-like attachments (#199)\n- `as_bytes` DKIM signing bug (#194)\n- SMTP connection is now properly closed on any initialization failure (#180)\n- SMTP connection is now properly closed on failed login (#173)\n- Incorrect `isinstance` check in `parse_name_and_email_list` (#176)\n- Message encoding to bytes in SMTP backend (#152)\n- Unique filename generation for attachments\n- Regex escape sequence warning (#148)\n- Replaced deprecated `cgi` module with `email.message`\n- Coverage reports now correctly exclude `emails/testsuite/`\n\n### Maintenance\n\n- Removed vendored dkim package (~1400 lines)\n- Removed Python 2 compatibility code and helpers (#188, #197, #198)\n- Updated pre-commit hooks to current versions\n- Updated GitHub Actions to supported versions\n- Removed universal wheel flag (py3-only)\n- Cleaned up documentation and project metadata\n- Added Python 3.12 to test matrix (#169)\n\n## 0.6 — 2019-07-14\n\nLast release before the changelog was introduced.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2013-2015 Sergey Lavrinenko\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.rst LICENSE requirements.txt\ninclude emails/py.typed\n"
  },
  {
    "path": "Makefile",
    "content": "\nDOCS_PYTHON = .venv/bin/python\nDOCS_SOURCE = docs\nDOCS_BUILD = $(DOCS_SOURCE)/_build/html\nSPHINXOPTS ?=\n\n.PHONY: clean docs test pypi codeql-db codeql-analyze codeql-clean\n\nclean:\n\tfind . -name '*.pyc'  -exec rm -f {} \\;\n\tfind . -name '*.py~'  -exec rm -f {} \\;\n\tfind . -name '__pycache__'  -exec rm -rf {} \\;\n\tfind . -name '.coverage.*' -exec rm -rf {} \\;\n\trm -rf build dist emails.egg-info tmp-emails _files $(DOCS_SOURCE)/_build\n\ndocs:\n\t$(DOCS_PYTHON) -m sphinx -b html $(SPHINXOPTS) $(DOCS_SOURCE) $(DOCS_BUILD)\n\t@echo\n\t@echo \"Build finished. Open $(DOCS_BUILD)/index.html\"\n\ntest:\n\ttox\n\npypi:\n\tpython setup.py sdist bdist_wheel upload\n\nCODEQL_DB = .codeql-db\nCODEQL_PYTHON = .venv/bin/python\n\ncodeql-db:\n\trm -rf $(CODEQL_DB)\n\tcodeql database create $(CODEQL_DB) --language=python --source-root=. \\\n\t\t--extractor-option=python.python_executable=$(CODEQL_PYTHON)\n\ncodeql-analyze: codeql-db\n\tcodeql pack download codeql/python-queries\n\tcodeql database analyze $(CODEQL_DB) codeql/python-queries \\\n\t\t--format=sarif-latest --output=codeql-results.sarif\n\ncodeql-clean:\n\trm -rf $(CODEQL_DB) codeql-results.sarif\n"
  },
  {
    "path": "README.rst",
    "content": "python-emails\n=============\n\n.. |pypi| image:: https://img.shields.io/pypi/v/emails.svg\n   :target: https://pypi.org/project/emails/\n   :alt: PyPI version\n\n.. |python| image:: https://img.shields.io/pypi/pyversions/emails.svg\n   :target: https://pypi.org/project/emails/\n   :alt: Python versions\n\n.. |tests| image:: https://github.com/lavr/python-emails/workflows/Tests/badge.svg?branch=master\n   :target: https://github.com/lavr/python-emails/actions?query=workflow%3ATests\n   :alt: Test status\n\n.. |docs| image:: https://readthedocs.org/projects/python-emails/badge/?version=latest\n   :target: https://python-emails.readthedocs.io/\n   :alt: Documentation status\n\n.. |license| image:: https://img.shields.io/pypi/l/emails.svg\n   :target: https://github.com/lavr/python-emails/blob/master/LICENSE\n   :alt: License\n\n|pypi| |python| |tests| |docs| |license|\n\nBuild, transform, and send emails in Python with a high-level API.\n\n``python-emails`` helps you compose HTML and plain-text messages, attach files,\nembed inline images, render templates, apply HTML transformations, sign with\nDKIM, and send through SMTP without hand-building MIME trees.\n\n\nWhy python-emails\n-----------------\n\n- A concise API over ``email`` and ``smtplib``\n- HTML and plain-text messages in one object\n- File attachments and inline images\n- CSS inlining, image embedding, and HTML cleanup\n- Jinja2, Mako, and string template support\n- DKIM signing\n- Loaders for URLs, HTML files, directories, ZIP archives, and RFC 822 messages\n- SMTP sending with SSL/TLS support\n- Async sending via ``aiosmtplib``\n\n\nQuick Example\n-------------\n\n.. code-block:: python\n\n    import emails\n\n    message = emails.html(\n        subject=\"Your receipt\",\n        html=\"<p>Hello!</p><p>Your payment was received.</p>\",\n        mail_from=(\"Billing\", \"billing@example.com\"),\n    )\n    message.attach(filename=\"receipt.pdf\", data=open(\"receipt.pdf\", \"rb\"))\n\n    response = message.send(\n        to=\"customer@example.com\",\n        smtp={\n            \"host\": \"smtp.example.com\",\n            \"port\": 587,\n            \"tls\": True,\n            \"user\": \"billing@example.com\",\n            \"password\": \"app-password\",\n        },\n    )\n    assert response.status_code == 250\n\n\nInstallation\n------------\n\nInstall the lightweight core:\n\n.. code-block:: bash\n\n    pip install emails\n\nInstall HTML transformation features such as CSS inlining, image embedding,\nand loading from URLs or files:\n\n.. code-block:: bash\n\n    pip install \"emails[html]\"\n\nInstall Jinja2 template support for the ``JinjaTemplate`` class:\n\n.. code-block:: bash\n\n    pip install \"emails[jinja]\"\n\nInstall async SMTP sending support for ``send_async()``:\n\n.. code-block:: bash\n\n    pip install \"emails[async]\"\n\n\nCommon Tasks\n------------\n\n- Build and send your first message:\n  `Quickstart <https://python-emails.readthedocs.io/en/latest/quickstart.html>`_\n- Configure installation extras:\n  `Install guide <https://python-emails.readthedocs.io/en/latest/install.html>`_\n- Inline CSS, embed images, and customize HTML processing:\n  `Advanced Usage <https://python-emails.readthedocs.io/en/latest/advanced.html>`_\n- Learn the full public API:\n  `API Reference <https://python-emails.readthedocs.io/en/latest/api.html>`_\n- Troubleshoot common scenarios:\n  `FAQ <https://python-emails.readthedocs.io/en/latest/faq.html>`_\n- Explore alternatives and related projects:\n  `Links <https://python-emails.readthedocs.io/en/latest/links.html>`_\n\n\nWhat You Get\n------------\n\n- Message composition for HTML, plain text, headers, CC/BCC, and Reply-To\n- Attachments, inline images, and MIME generation\n- Template rendering in ``html``, ``text``, and ``subject``\n- HTML transformations through ``message.transform()``\n- SMTP delivery through config dicts or reusable backend objects\n- Django integration via ``DjangoMessage``\n- Flask integration via `flask-emails <https://github.com/lavr/flask-emails>`_\n\n\nWhen To Use It\n--------------\n\nUse ``python-emails`` when you need more than a minimal plain-text SMTP call:\nHTML emails, attachments, inline images, template rendering, DKIM, message\nloading from external sources, or a cleaner API than hand-written\n``email.mime`` code.\n\nIf you only need to send a very small plain-text message and want zero\ndependencies, the standard library may be enough.\n\n\nDocumentation\n-------------\n\n- `Documentation home <https://python-emails.readthedocs.io/>`_\n- `Quickstart <https://python-emails.readthedocs.io/en/latest/quickstart.html>`_\n- `Advanced Usage <https://python-emails.readthedocs.io/en/latest/advanced.html>`_\n- `API Reference <https://python-emails.readthedocs.io/en/latest/api.html>`_\n- `FAQ <https://python-emails.readthedocs.io/en/latest/faq.html>`_\n\n\nProject Status\n--------------\n\n``python-emails`` is production/stable software and currently supports\nPython 3.10 through 3.14.\n\n\nContributing\n------------\n\nIssues and pull requests are welcome.\n\n- `Report a bug or request a feature <https://github.com/lavr/python-emails/issues>`_\n- `Source code on GitHub <https://github.com/lavr/python-emails>`_\n- `How to Help <https://python-emails.readthedocs.io/en/latest/howtohelp.html>`_\n\n\nLicense\n-------\n\nApache 2.0. See `LICENSE <https://github.com/lavr/python-emails/blob/master/LICENSE>`_.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(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/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/python-emails.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/python-emails.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/python-emails\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-emails\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "docs/_static/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/advanced.rst",
    "content": "Advanced Usage\n==============\n\nThis section covers advanced features and usage patterns of ``python-emails``.\n\n\nSMTP Connections\n----------------\n\nBy default, :meth:`~emails.Message.send` accepts an ``smtp`` dict and\nmanages the connection internally:\n\n.. code-block:: python\n\n    response = message.send(\n        to=\"user@example.com\",\n        smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n              \"user\": \"me\", \"password\": \"secret\"}\n    )\n\nFor more control, you can use :class:`~emails.backend.smtp.SMTPBackend`\ndirectly.\n\n\nReusing Connections\n~~~~~~~~~~~~~~~~~~~\n\nWhen you call :meth:`~emails.Message.send` with the same ``smtp`` dict\non the same message, the library automatically reuses the SMTP connection\nthrough an internal pool. Connections with identical parameters share\na backend:\n\n.. code-block:: python\n\n    smtp_config = {\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n                   \"user\": \"me\", \"password\": \"secret\"}\n\n    # These two calls reuse the same underlying SMTP connection\n    message.send(to=\"alice@example.com\", smtp=smtp_config)\n    message.send(to=\"bob@example.com\", smtp=smtp_config)\n\nFor explicit connection management, create an :class:`SMTPBackend` instance\nand pass it instead of a dict. The backend supports context managers:\n\n.. code-block:: python\n\n    from emails.backend.smtp import SMTPBackend\n\n    with SMTPBackend(host=\"smtp.example.com\", port=587,\n                     tls=True, user=\"me\", password=\"secret\") as backend:\n        for recipient in recipients:\n            message.send(to=recipient, smtp=backend)\n    # Connection is closed automatically\n\n\nSSL vs STARTTLS\n~~~~~~~~~~~~~~~\n\nThe library supports two encryption modes:\n\n- **Implicit SSL** (``ssl=True``): Connects over TLS from the start.\n  Typically used with port 465.\n\n  .. code-block:: python\n\n      message.send(smtp={\"host\": \"mail.example.com\", \"port\": 465, \"ssl\": True,\n                         \"user\": \"me\", \"password\": \"secret\"})\n\n- **STARTTLS** (``tls=True``): Connects in plain text, then upgrades to TLS.\n  Typically used with port 587.\n\n  .. code-block:: python\n\n      message.send(smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n                         \"user\": \"me\", \"password\": \"secret\"})\n\nYou cannot set both ``ssl`` and ``tls`` to ``True`` -- this raises a\n``ValueError``.\n\n\nTimeouts\n~~~~~~~~\n\nThe default socket timeout is 5 seconds. You can change it with the\n``timeout`` parameter:\n\n.. code-block:: python\n\n    message.send(smtp={\"host\": \"smtp.example.com\", \"timeout\": 30})\n\n\nDebugging\n~~~~~~~~~\n\nEnable SMTP protocol debugging to see the full conversation with the\nserver on stdout:\n\n.. code-block:: python\n\n    message.send(smtp={\"host\": \"smtp.example.com\", \"debug\": 1})\n\n\nAll SMTP Parameters\n~~~~~~~~~~~~~~~~~~~\n\nThe full list of parameters accepted in the ``smtp`` dict (or as\n:class:`SMTPBackend` constructor arguments):\n\n- ``host`` -- SMTP server hostname\n- ``port`` -- server port (int)\n- ``ssl`` -- use implicit SSL/TLS (for port 465)\n- ``tls`` -- use STARTTLS (for port 587)\n- ``user`` -- authentication username\n- ``password`` -- authentication password\n- ``timeout`` -- socket timeout in seconds (default: ``5``)\n- ``debug`` -- debug level (``0`` = off, ``1`` = verbose)\n- ``fail_silently`` -- if ``True`` (default), return errors in the response\n  instead of raising exceptions\n- ``local_hostname`` -- FQDN for the EHLO/HELO command (auto-detected\n  if not set)\n- ``keyfile`` -- path to SSL key file\n- ``certfile`` -- path to SSL certificate file\n- ``mail_options`` -- list of ESMTP MAIL command options\n  (e.g., ``[\"smtputf8\"]``)\n\n\nHTML Transformations\n--------------------\n\nThe :meth:`~emails.Message.transform` method processes the HTML body\nbefore sending -- inlining CSS, loading images, removing unsafe tags,\nand more.\n\n.. code-block:: python\n\n    message = emails.Message(\n        html=\"<style>h1{color:red}</style><h1>Hello!</h1>\"\n    )\n    message.transform()\n\nAfter transformation, the inline style is applied directly:\n\n.. code-block:: python\n\n    print(message.html)\n    # <html><head></head><body><h1 style=\"color:red\">Hello!</h1></body></html>\n\n\nParameters\n~~~~~~~~~~\n\n:meth:`~emails.Message.transform` accepts the following keyword arguments:\n\n``css_inline`` (default: ``True``)\n    Inline CSS styles using `premailer <https://github.com/peterbe/premailer>`_.\n    External stylesheets referenced in ``<link>`` tags are loaded and\n    converted to inline ``style`` attributes.\n\n``remove_unsafe_tags`` (default: ``True``)\n    Remove potentially dangerous HTML tags: ``<script>``, ``<object>``,\n    ``<iframe>``, ``<frame>``, ``<base>``, ``<meta>``, ``<link>``,\n    ``<style>``.\n\n``set_content_type_meta`` (default: ``True``)\n    Add a ``<meta http-equiv=\"Content-Type\">`` tag to the ``<head>``\n    with the message's charset.\n\n``load_images`` (default: ``True``)\n    Load images referenced in the HTML and embed them as message\n    attachments. Accepts ``True``, ``False``, or a callable for custom\n    filtering (see below).\n\n``images_inline`` (default: ``False``)\n    When ``True``, loaded images are embedded as inline attachments\n    using ``cid:`` references instead of regular attachments.\n\nThe following parameters are **deprecated** and have no effect:\n\n``make_links_absolute``\n    Premailer always makes links absolute. Passing ``False`` triggers\n    a ``DeprecationWarning``.\n\n``update_stylesheet``\n    Premailer does not support this feature. Passing ``True`` triggers\n    a ``DeprecationWarning``.\n\n\nCustom Image Filtering\n~~~~~~~~~~~~~~~~~~~~~~\n\nPass a callable as ``load_images`` to control which images are loaded:\n\n.. code-block:: python\n\n    def should_load(element, hints=None, **kwargs):\n        # Skip tracking pixels\n        src = element.attrib.get(\"src\", \"\")\n        if \"track\" in src or \"pixel\" in src:\n            return False\n        return True\n\n    message.transform(load_images=should_load)\n\nYou can also use the ``data-emails`` attribute in your HTML to control\nindividual images:\n\n- ``data-emails=\"ignore\"`` -- skip loading this image\n- ``data-emails=\"inline\"`` -- load as an inline attachment\n\n\nCustom Link and Image Transformations\n--------------------------------------\n\nFor more specific transformations, access the ``transformer`` property\ndirectly.\n\n\nTransforming Image URLs\n~~~~~~~~~~~~~~~~~~~~~~~\n\n:meth:`~emails.transformer.HTMLParser.apply_to_images` applies a function\nto all image references in the HTML -- ``<img src>``, ``background``\nattributes, and CSS ``url()`` values in ``style`` attributes:\n\n.. code-block:: python\n\n    message = emails.Message(html='<img src=\"promo.png\">')\n    message.transformer.apply_to_images(\n        func=lambda src, **kw: \"https://cdn.example.com/images/\" + src\n    )\n    message.transformer.save()\n\n    print(message.html)\n    # <html><body><img src=\"https://cdn.example.com/images/promo.png\"></body></html>\n\nThe callback receives ``uri`` (the current URL) and ``element`` (the lxml\nelement), and should return the new URL.\n\nYou can limit the scope with keyword arguments:\n\n- ``images=True`` -- apply to ``<img src>`` (default: ``True``)\n- ``backgrounds=True`` -- apply to ``background`` attributes (default: ``True``)\n- ``styles_uri=True`` -- apply to CSS ``url()`` in style attributes (default: ``True``)\n\n\nTransforming Link URLs\n~~~~~~~~~~~~~~~~~~~~~~\n\n:meth:`~emails.transformer.HTMLParser.apply_to_links` applies a function\nto all ``<a href>`` values:\n\n.. code-block:: python\n\n    message = emails.Message(html='<a href=\"/about\">About</a>')\n    message.transformer.apply_to_links(\n        func=lambda href, **kw: \"https://example.com\" + href\n    )\n    message.transformer.save()\n\n    print(message.html)\n    # <html><body><a href=\"https://example.com/about\">About</a></body></html>\n\nAlways call ``message.transformer.save()`` after using ``apply_to_images``\nor ``apply_to_links`` to update the message's HTML body.\n\n\nMaking Images Inline Manually\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can mark individual attachments as inline and synchronize the HTML\nreferences:\n\n.. code-block:: python\n\n    message = emails.Message(html='<img src=\"promo.png\">')\n    message.attach(filename=\"promo.png\", data=open(\"promo.png\", \"rb\"))\n    message.attachments[\"promo.png\"].is_inline = True\n    message.transformer.synchronize_inline_images()\n    message.transformer.save()\n\n    print(message.html)\n    # <html><body><img src=\"cid:promo.png\"></body></html>\n\n\nLoaders\n-------\n\nLoader functions create :class:`Message` instances from various sources,\nautomatically handling HTML parsing, CSS inlining, and image embedding.\nAll loaders are in the ``emails.loader`` module.\n\n\nLoading from a URL\n~~~~~~~~~~~~~~~~~~\n\n:func:`~emails.loader.from_url` fetches an HTML page and embeds all\nreferenced images and stylesheets:\n\n.. code-block:: python\n\n    import emails.loader\n\n    message = emails.loader.from_url(\n        url=\"https://example.com/newsletter/2024-01/index.html\",\n        requests_params={\"timeout\": 30}\n    )\n\nThe ``requests_params`` dict is passed to the underlying HTTP requests\n(for controlling timeouts, SSL verification, headers, etc.).\n\n\nLoading from a ZIP Archive\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:func:`~emails.loader.from_zip` reads an HTML file and its resources\nfrom a ZIP archive. The archive must contain at least one ``.html`` file:\n\n.. code-block:: python\n\n    message = emails.loader.from_zip(\n        open(\"template.zip\", \"rb\"),\n        message_params={\"subject\": \"Newsletter\", \"mail_from\": \"news@example.com\"}\n    )\n\n\nLoading from a Directory\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n:func:`~emails.loader.from_directory` loads from a local directory.\nIt looks for ``index.html`` (or ``index.htm``) automatically:\n\n.. code-block:: python\n\n    message = emails.loader.from_directory(\n        \"/path/to/email-template/\",\n        message_params={\"subject\": \"Welcome\", \"mail_from\": \"hello@example.com\"}\n    )\n\n\nLoading from a File\n~~~~~~~~~~~~~~~~~~~\n\n:func:`~emails.loader.from_file` loads from a single HTML file. Images\nand CSS are resolved relative to the file's directory:\n\n.. code-block:: python\n\n    message = emails.loader.from_file(\"/path/to/email-template/welcome.html\")\n\n\nLoading from an .eml File\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:func:`~emails.loader.from_rfc822` parses an RFC 822 email (e.g., a\n``.eml`` file). Set ``parse_headers=True`` to copy Subject, From, To,\nand other headers:\n\n.. code-block:: python\n\n    message = emails.loader.from_rfc822(\n        open(\"archived.eml\", \"rb\").read(),\n        parse_headers=True\n    )\n\nThis loader is primarily intended for demonstration and testing purposes.\n\n\nWhen to Use Which Loader\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- **from_html** -- you already have HTML as a string and want to\n  process it (inline CSS, embed images)\n- **from_url** -- the email template is hosted on a web server\n- **from_directory** -- the template is a local folder with HTML, images,\n  and CSS files\n- **from_zip** -- the template is distributed as a ZIP archive\n- **from_file** -- you have a single local HTML file\n- **from_rfc822** -- you want to re-create a message from an existing\n  ``.eml`` file\n\n\nDjango Integration\n------------------\n\n``python-emails`` provides :class:`~emails.django.DjangoMessage`, a\n:class:`Message` subclass that sends through Django's email backend.\n\n.. code-block:: python\n\n    from emails.django import DjangoMessage\n\n    message = DjangoMessage(\n        html=\"<p>Hello {{ name }}!</p>\",\n        subject=\"Welcome\",\n        mail_from=\"noreply@example.com\"\n    )\n    result = message.send(to=\"user@example.com\", context={\"name\": \"Alice\"})\n\nKey differences from :class:`Message`:\n\n- Uses ``context`` instead of ``render`` for template variables.\n- Uses Django's configured email backend (``django.core.mail.get_connection()``)\n  instead of an ``smtp`` dict.\n- Returns ``1`` on success and ``0`` on failure (matching Django's\n  ``send_mail`` convention).\n- Accepts an optional ``connection`` parameter for a custom Django email\n  backend connection.\n\nUsing a custom Django connection:\n\n.. code-block:: python\n\n    from django.core.mail import get_connection\n    from emails.django import DjangoMessage\n\n    message = DjangoMessage(\n        html=\"<p>Notification</p>\",\n        subject=\"Alert\",\n        mail_from=\"alerts@example.com\"\n    )\n\n    connection = get_connection(backend=\"django.core.mail.backends.smtp.EmailBackend\")\n    message.send(to=\"admin@example.com\", connection=connection)\n\nDjango email settings (``EMAIL_HOST``, ``EMAIL_PORT``, etc.) are used\nautomatically when no explicit connection is provided.\n\n\nFlask Integration\n-----------------\n\nFor Flask applications, use the\n`flask-emails <https://github.com/lavr/flask-emails>`_ extension, which\nprovides Flask-specific integration (app factory support, configuration\nfrom Flask config, etc.):\n\n.. code-block:: python\n\n    from flask_emails import Message\n\n    message = Message(\n        html=\"<p>Hello!</p>\",\n        subject=\"Test\",\n        mail_from=\"sender@example.com\"\n    )\n    message.send(to=\"user@example.com\")\n\nInstall with::\n\n    pip install flask-emails\n\nRefer to the `flask-emails documentation <https://github.com/lavr/flask-emails>`_\nfor configuration details.\n\n\nCharset and Encoding\n--------------------\n\n``python-emails`` uses two separate encoding settings:\n\n- ``charset`` -- encoding for the message body (default: ``'utf-8'``)\n- ``headers_encoding`` -- encoding for email headers (default: ``'ascii'``)\n\n\nChanging the Body Charset\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nFor messages in specific encodings (e.g., Cyrillic), set the ``charset``\nparameter:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=\"<p>Content in specific encoding</p>\",\n        charset=\"windows-1251\",\n        mail_from=\"sender@example.com\"\n    )\n\nThe library automatically registers proper encoding behaviors for common\ncharsets including ``utf-8``, ``windows-1251``, and ``koi8-r``.\n\n\nInternationalized Domain Names (IDN)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nEmail addresses with internationalized domain names work with the\nstandard address format. The library handles encoding automatically:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=\"<p>Hello!</p>\",\n        mail_from=(\"Sender\", \"user@example.com\"),\n        mail_to=(\"Recipient\", \"user@example.com\")\n    )\n\n\nHeaders\n-------\n\nCustom Headers\n~~~~~~~~~~~~~~\n\nPass a ``headers`` dict when creating a message to add custom email\nheaders:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=\"<p>Hello!</p>\",\n        subject=\"Test\",\n        mail_from=\"sender@example.com\",\n        headers={\n            \"X-Mailer\": \"python-emails\",\n            \"X-Priority\": \"1\",\n            \"List-Unsubscribe\": \"<mailto:unsubscribe@example.com>\"\n        }\n    )\n\nNon-ASCII characters in header values are automatically encoded\naccording to RFC 2047.\n\nHeader values are validated -- newline characters (``\\n``, ``\\r``)\nraise :exc:`~emails.BadHeaderError` to prevent header injection attacks.\n\n\nReply-To, CC, and BCC\n~~~~~~~~~~~~~~~~~~~~~~\n\nThese fields accept the same formats as ``mail_from`` and ``mail_to`` --\na string, a ``(name, email)`` tuple, or a list of either:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=\"<p>Hello!</p>\",\n        subject=\"Team update\",\n        mail_from=(\"Alice\", \"alice@example.com\"),\n        mail_to=[(\"Bob\", \"bob@example.com\"), (\"Carol\", \"carol@example.com\")],\n        cc=\"dave@example.com\",\n        bcc=[\"eve@example.com\", \"frank@example.com\"],\n        reply_to=(\"Alice\", \"alice-reply@example.com\")\n    )\n\n- **CC** recipients are visible to all recipients in the email headers.\n- **BCC** recipients receive the message but are not listed in the headers.\n- **Reply-To** sets the address that email clients use when the recipient\n  clicks \"Reply\".\n"
  },
  {
    "path": "docs/api.rst",
    "content": "API Reference\n=============\n\nThis section documents the public API of the ``python-emails`` library.\n\n\nMessage\n-------\n\nThe :class:`Message` class is the main entry point for creating and sending emails.\n\n.. class:: Message(\\*\\*kwargs)\n\n   Create a new email message.\n\n   :param html: HTML body content (string or file-like object).\n   :type html: str or None\n   :param text: Plain text body content (string or file-like object).\n   :type text: str or None\n   :param subject: Email subject line. Supports template rendering.\n   :type subject: str or None\n   :param mail_from: Sender address. Accepts a string ``\"user@example.com\"`` or\n       a tuple ``(\"Display Name\", \"user@example.com\")``.\n   :type mail_from: str or tuple or None\n   :param mail_to: Recipient address(es). Accepts a string, tuple, or list of strings/tuples.\n   :type mail_to: str or tuple or list or None\n   :param cc: CC recipient(s). Same format as ``mail_to``.\n   :type cc: str or tuple or list or None\n   :param bcc: BCC recipient(s). Same format as ``mail_to``.\n   :type bcc: str or tuple or list or None\n   :param reply_to: Reply-To address(es). Same format as ``mail_to``.\n   :type reply_to: str or tuple or list or None\n   :param headers: Custom email headers as a dictionary.\n   :type headers: dict or None\n   :param headers_encoding: Encoding for email headers (default: ``'ascii'``).\n   :type headers_encoding: str or None\n   :param attachments: List of attachments (dicts or :class:`BaseFile` objects).\n   :type attachments: list or None\n   :param charset: Message character set (default: ``'utf-8'``).\n   :type charset: str or None\n   :param message_id: Message-ID header value. Can be a string, a :class:`MessageID` instance,\n       ``False`` to omit, or ``None`` to auto-generate.\n   :type message_id: str or :class:`~emails.utils.MessageID` or bool or None\n   :param date: Date header value. Accepts a string, :class:`~datetime.datetime`, float (timestamp),\n       ``False`` to omit, or a callable that returns one of these types.\n   :type date: str or datetime or float or bool or callable or None\n\n   Example::\n\n       import emails\n\n       msg = emails.Message(\n           html=\"<p>Hello, World!</p>\",\n           subject=\"Test Email\",\n           mail_from=(\"Sender\", \"sender@example.com\"),\n           mail_to=\"recipient@example.com\"\n       )\n\n\nMessage Methods\n~~~~~~~~~~~~~~~\n\n.. 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)\n\n   Send the message via SMTP.\n\n   :param to: Override recipient address(es).\n   :param set_mail_to: If ``True``, update the message's ``mail_to`` with ``to``.\n   :param mail_from: Override sender address.\n   :param set_mail_from: If ``True``, update the message's ``mail_from``.\n   :param render: Dictionary of template variables for rendering.\n   :param smtp_mail_options: SMTP MAIL command options.\n   :param smtp_rcpt_options: SMTP RCPT command options.\n   :param smtp: SMTP configuration. Either a dict with connection parameters\n       (``host``, ``port``, ``ssl``, ``tls``, ``user``, ``password``, ``timeout``)\n       or an :class:`SMTPBackend` instance.\n   :returns: :class:`SMTPResponse` or ``None``\n\n   Example::\n\n       response = msg.send(\n           to=\"user@example.com\",\n           smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n                 \"user\": \"login\", \"password\": \"secret\"}\n       )\n\n.. 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)\n\n   Send the message via SMTP asynchronously. Requires ``aiosmtplib``\n   (install with ``pip install \"emails[async]\"``).\n\n   Parameters are the same as :meth:`send`, except ``smtp`` accepts a dict\n   or an :class:`AsyncSMTPBackend` instance.\n\n   When ``smtp`` is a dict, a temporary :class:`AsyncSMTPBackend` is created\n   and closed after sending. When an existing backend is passed, the caller\n   is responsible for closing it.\n\n   :returns: :class:`SMTPResponse` or ``None``\n\n   Example::\n\n       response = await msg.send_async(\n           to=\"user@example.com\",\n           smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n                 \"user\": \"login\", \"password\": \"secret\"}\n       )\n\n   Using a shared backend for multiple sends::\n\n       from emails.backend.smtp.aio_backend import AsyncSMTPBackend\n\n       async with AsyncSMTPBackend(host=\"smtp.example.com\", port=587,\n                                   tls=True, user=\"login\",\n                                   password=\"secret\") as backend:\n           for msg in messages:\n               await msg.send_async(smtp=backend)\n\n.. method:: Message.attach(\\*\\*kwargs)\n\n   Attach a file to the message. Sets ``content_disposition`` to ``'attachment'``\n   by default.\n\n   :param filename: Name of the attached file.\n   :param data: File content as bytes or a file-like object.\n   :param content_disposition: ``'attachment'`` (default) or ``'inline'``.\n   :param mime_type: MIME type of the file. Auto-detected from filename if not specified.\n\n   Example::\n\n       msg.attach(filename=\"report.pdf\", data=open(\"report.pdf\", \"rb\"))\n       msg.attach(filename=\"logo.png\", data=img_data, content_disposition=\"inline\")\n\n.. method:: Message.render(\\*\\*kwargs)\n\n   Set template rendering data. Template variables are substituted when\n   accessing ``html_body``, ``text_body``, or ``subject``.\n\n   :param kwargs: Key-value pairs used as template context.\n\n   Example::\n\n       msg = emails.Message(\n           html=emails.template.JinjaTemplate(\"<p>Hello {{ name }}</p>\"),\n           subject=emails.template.JinjaTemplate(\"Welcome, {{ name }}\")\n       )\n       msg.render(name=\"World\")\n\n.. method:: Message.as_string(message_cls=None)\n\n   Return the message as a string, including DKIM signature if configured.\n\n   :param message_cls: Optional custom MIME message class.\n   :returns: Message as a string.\n   :rtype: str\n\n.. method:: Message.as_bytes(message_cls=None)\n\n   Return the message as bytes, including DKIM signature if configured.\n\n   :param message_cls: Optional custom MIME message class.\n   :returns: Message as bytes.\n   :rtype: bytes\n\n.. method:: Message.as_message(message_cls=None)\n\n   Return the underlying MIME message object.\n\n   :param message_cls: Optional custom MIME message class.\n   :returns: MIME message object.\n\n.. method:: Message.transform(\\*\\*kwargs)\n\n   Apply HTML transformations to the message body. Loads and processes the HTML\n   content through the transformer.\n\n   See the HTML Transformations section for available parameters.\n\n.. method:: Message.dkim(key, domain, selector, ignore_sign_errors=False, \\*\\*kwargs)\n\n   Configure DKIM signing for the message. The signature is applied when\n   the message is serialized via :meth:`as_string`, :meth:`as_bytes`,\n   or :meth:`send`.\n\n   This method is also available as :meth:`sign`.\n\n   :param key: Private key for signing (PEM format). String, bytes, or file-like object.\n   :param domain: DKIM domain (e.g., ``\"example.com\"``).\n   :param selector: DKIM selector (e.g., ``\"default\"``).\n   :param ignore_sign_errors: If ``True``, suppress signing exceptions.\n   :returns: The message instance (for chaining).\n   :rtype: Message\n\n   Example::\n\n       msg.dkim(key=open(\"private.pem\"), domain=\"example.com\", selector=\"default\")\n\n\nMessage Properties\n~~~~~~~~~~~~~~~~~~\n\n.. attribute:: Message.html\n\n   Get or set the HTML body content.\n\n.. attribute:: Message.text\n\n   Get or set the plain text body content.\n\n.. attribute:: Message.html_body\n\n   The rendered HTML body (read-only). If templates are used, returns the\n   rendered result; otherwise returns the raw HTML.\n\n.. attribute:: Message.text_body\n\n   The rendered text body (read-only). If templates are used, returns the\n   rendered result; otherwise returns the raw text.\n\n.. attribute:: Message.mail_from\n\n   Get or set the sender address. Returns a ``(name, email)`` tuple.\n\n.. attribute:: Message.mail_to\n\n   Get or set the recipient address(es). Returns a list of ``(name, email)`` tuples.\n\n.. attribute:: Message.cc\n\n   Get or set CC recipient(s). Returns a list of ``(name, email)`` tuples.\n\n.. attribute:: Message.bcc\n\n   Get or set BCC recipient(s). Returns a list of ``(name, email)`` tuples.\n\n.. attribute:: Message.reply_to\n\n   Get or set Reply-To address(es). Returns a list of ``(name, email)`` tuples.\n\n.. attribute:: Message.subject\n\n   Get or set the email subject. Supports template rendering.\n\n.. attribute:: Message.message_id\n\n   Get or set the Message-ID header value.\n\n.. attribute:: Message.date\n\n   Get or set the Date header value.\n\n.. attribute:: Message.charset\n\n   Get or set the message character set (default: ``'utf-8'``).\n\n.. attribute:: Message.headers_encoding\n\n   Get or set the encoding for email headers (default: ``'ascii'``).\n\n.. attribute:: Message.attachments\n\n   Access the attachment store (:class:`MemoryFileStore`). Lazily initialized.\n\n.. attribute:: Message.render_data\n\n   Get or set the template rendering context dictionary.\n\n.. attribute:: Message.transformer\n\n   Access the HTML transformer for custom image/link transformations.\n   Lazily created on first access. See the :doc:`HTML Transformations <transformations>`\n   section for usage examples.\n\n\nSMTPResponse\n------------\n\nReturned by :meth:`Message.send`. Contains information about the SMTP transaction.\n\n.. class:: SMTPResponse\n\n   .. attribute:: status_code\n\n      The SMTP status code from the last command (e.g., ``250`` for success).\n      ``None`` if the transaction was not completed.\n\n   .. attribute:: status_text\n\n      The SMTP status text from the last command, as bytes.\n\n   .. attribute:: success\n\n      ``True`` if the message was sent successfully (status code is ``250``\n      and the transaction completed).\n\n   .. attribute:: error\n\n      The exception object if an error occurred, or ``None``.\n\n   .. attribute:: refused_recipients\n\n      A dictionary mapping refused recipient email addresses to\n      ``(code, message)`` tuples.\n\n   .. attribute:: last_command\n\n      The last SMTP command that was sent (e.g., ``'mail'``, ``'rcpt'``, ``'data'``).\n\n   Example::\n\n       response = msg.send(smtp={\"host\": \"localhost\"})\n       if response.success:\n           print(\"Sent!\")\n       else:\n           print(f\"Failed: {response.status_code} {response.status_text}\")\n           if response.error:\n               print(f\"Error: {response.error}\")\n           if response.refused_recipients:\n               print(f\"Refused: {response.refused_recipients}\")\n\n\nAsyncSMTPBackend\n----------------\n\nFor async sending via :meth:`Message.send_async`. Requires ``aiosmtplib``\n(install with ``pip install \"emails[async]\"``).\n\n.. class:: emails.backend.smtp.aio_backend.AsyncSMTPBackend(ssl=False, fail_silently=True, mail_options=None, \\*\\*kwargs)\n\n   Manages an async SMTP connection. Supports ``async with`` for automatic cleanup.\n\n   :param host: SMTP server hostname.\n   :param port: SMTP server port.\n   :param ssl: Use implicit TLS (SMTPS).\n   :param tls: Use STARTTLS after connecting.\n   :param user: SMTP username for authentication.\n   :param password: SMTP password for authentication.\n   :param timeout: Connection timeout in seconds (default: ``5``).\n   :param fail_silently: If ``True`` (default), SMTP errors are captured in the\n       response rather than raised.\n   :param mail_options: Default SMTP MAIL command options.\n\n   .. method:: sendmail(from_addr, to_addrs, msg, mail_options=None, rcpt_options=None)\n      :async:\n\n      Send a message. Automatically retries once on server disconnect.\n\n      :returns: :class:`SMTPResponse` or ``None``\n\n   .. method:: close()\n      :async:\n\n      Close the SMTP connection.\n\n   Example::\n\n       from emails.backend.smtp.aio_backend import AsyncSMTPBackend\n\n       async with AsyncSMTPBackend(host=\"smtp.example.com\", port=587,\n                                   tls=True, user=\"me\",\n                                   password=\"secret\") as backend:\n           response = await backend.sendmail(\n               from_addr=\"sender@example.com\",\n               to_addrs=[\"recipient@example.com\"],\n               msg=message\n           )\n\n\nLoaders\n-------\n\nLoader functions create :class:`Message` instances from various sources.\n\nAll loaders are available in the ``emails.loader`` module.\n\n.. 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)\n\n   Create a message from an HTML string. Images and stylesheets referenced\n   in the HTML can be automatically loaded and embedded.\n\n   :param html: HTML content as a string.\n   :param text: Optional plain text alternative.\n   :param base_url: Base URL for resolving relative URLs in the HTML.\n   :param message_params: Additional parameters passed to the Message constructor.\n   :param local_loader: A loader instance for resolving local file references.\n   :param template_cls: Template class to use for the HTML body.\n   :param message_cls: Custom Message class to instantiate.\n   :param source_filename: Filename hint for the source HTML.\n   :param requests_params: Parameters passed to HTTP requests when fetching resources.\n   :param kwargs: Additional transformer options.\n   :returns: A :class:`Message` instance.\n\n   ``from_string`` is an alias for this function.\n\n.. function:: emails.loader.from_url(url, requests_params=None, \\*\\*kwargs)\n\n   Create a message by downloading an HTML page from a URL.\n   Images and stylesheets are fetched and embedded.\n\n   :param url: URL of the HTML page.\n   :param requests_params: Parameters passed to HTTP requests.\n   :param kwargs: Additional transformer options.\n   :returns: A :class:`Message` instance.\n\n   ``load_url`` is an alias for this function.\n\n.. function:: emails.loader.from_directory(directory, loader_cls=None, \\*\\*kwargs)\n\n   Create a message from a local directory. The directory should contain\n   an HTML file and any referenced images or attachments.\n\n   :param directory: Path to the directory.\n   :param loader_cls: Custom loader class.\n   :param kwargs: Additional options (``html_filename``, ``text_filename``, ``message_params``).\n   :returns: A :class:`Message` instance.\n\n.. function:: emails.loader.from_zip(zip_file, loader_cls=None, \\*\\*kwargs)\n\n   Create a message from a ZIP archive containing HTML and resources.\n\n   :param zip_file: Path to ZIP file or a file-like object.\n   :param loader_cls: Custom loader class.\n   :param kwargs: Additional options (``html_filename``, ``text_filename``, ``message_params``).\n   :returns: A :class:`Message` instance.\n\n.. function:: emails.loader.from_file(filename, \\*\\*kwargs)\n\n   Create a message from a single HTML file.\n\n   :param filename: Path to the HTML file.\n   :param kwargs: Additional options (``message_params``).\n   :returns: A :class:`Message` instance.\n\n.. function:: emails.loader.from_rfc822(msg, loader_cls=None, message_params=None, parse_headers=False)\n\n   Create a message from an RFC 822 email object (e.g., from :mod:`email.message`).\n   Primarily intended for demonstration and testing purposes.\n\n   :param msg: An :class:`email.message.Message` object.\n   :param loader_cls: Custom loader class.\n   :param message_params: Additional parameters for the Message constructor.\n   :param parse_headers: If ``True``, parse and transfer email headers.\n   :returns: A :class:`Message` instance.\n\n\nLoader Exceptions\n~~~~~~~~~~~~~~~~~\n\n.. exception:: emails.loader.LoadError\n\n   Base exception for all loader errors.\n\n.. exception:: emails.loader.IndexFileNotFound\n\n   Raised when the loader cannot find an HTML index file in the source.\n   Subclass of :exc:`LoadError`.\n\n.. exception:: emails.loader.InvalidHtmlFile\n\n   Raised when the HTML content cannot be parsed.\n   Subclass of :exc:`LoadError`.\n\n\nTemplates\n---------\n\nTemplate classes allow dynamic content in email bodies and subjects. Pass a\ntemplate instance as the ``html``, ``text``, or ``subject`` parameter of\n:class:`Message`.\n\nInstall template dependencies with extras::\n\n    pip install \"emails[jinja]\"   # for JinjaTemplate\n\n.. class:: emails.template.JinjaTemplate(template_text, environment=None)\n\n   Template using `Jinja2 <https://jinja.palletsprojects.com/>`_ syntax.\n\n   :param template_text: Jinja2 template string.\n   :param environment: Optional :class:`jinja2.Environment` instance.\n\n   Example::\n\n       from emails.template import JinjaTemplate\n\n       msg = emails.Message(\n           html=JinjaTemplate(\"<p>Hello {{ name }}!</p>\"),\n           subject=JinjaTemplate(\"Welcome, {{ name }}\"),\n           mail_from=\"noreply@example.com\"\n       )\n       msg.send(render={\"name\": \"Alice\"}, smtp={\"host\": \"localhost\"})\n\n.. class:: emails.template.StringTemplate(template_text, safe_substitute=True)\n\n   Template using Python's :class:`string.Template` syntax (``$variable`` or\n   ``${variable}``).\n\n   :param template_text: Template string.\n   :param safe_substitute: If ``True`` (default), undefined variables are left\n       as-is. If ``False``, undefined variables raise :exc:`KeyError`.\n\n   Example::\n\n       from emails.template import StringTemplate\n\n       msg = emails.Message(\n           html=StringTemplate(\"<p>Hello $name!</p>\"),\n           mail_from=\"noreply@example.com\"\n       )\n\n.. class:: emails.template.MakoTemplate(template_text, \\*\\*kwargs)\n\n   Template using `Mako <https://www.makotemplates.org/>`_ syntax.\n   Requires the ``mako`` package.\n\n   :param template_text: Mako template string.\n   :param kwargs: Additional parameters passed to :class:`mako.template.Template`.\n\n\nDjangoMessage\n-------------\n\nA :class:`Message` subclass for use with Django's email backend.\n\n.. class:: emails.django.DjangoMessage(\\*\\*kwargs)\n\n   Accepts the same parameters as :class:`Message`. Integrates with\n   Django's email sending infrastructure.\n\n   .. method:: send(mail_to=None, set_mail_to=True, mail_from=None, set_mail_from=False, context=None, connection=None, to=None)\n\n      Send the message through Django's email backend.\n\n      :param context: Dictionary of template rendering variables\n          (equivalent to ``render`` in :meth:`Message.send`).\n      :param connection: A Django email backend connection instance\n          (e.g., from ``django.core.mail.get_connection()``).\n          If ``None``, uses the default backend.\n      :param to: Alias for ``mail_to``.\n      :returns: ``1`` if the message was sent successfully, ``0`` otherwise.\n      :rtype: int\n\n   Example::\n\n       from emails.django import DjangoMessage\n\n       msg = DjangoMessage(\n           html=\"<p>Hello {{ name }}</p>\",\n           subject=\"Welcome\",\n           mail_from=\"noreply@example.com\"\n       )\n       msg.send(to=\"user@example.com\", context={\"name\": \"Alice\"})\n\n\nDKIM\n----\n\nDKIM (DomainKeys Identified Mail) signing is configured via the\n:meth:`Message.dkim` method (or its alias :meth:`Message.sign`).\n\nParameters:\n\n- ``key`` -- Private key in PEM format. Accepts a string, bytes, or file-like object.\n- ``domain`` -- The signing domain (e.g., ``\"example.com\"``).\n- ``selector`` -- The DKIM selector (e.g., ``\"default\"``).\n- ``ignore_sign_errors`` -- If ``True``, silently ignore signing errors\n  instead of raising exceptions.\n- Additional keyword arguments are passed to the DKIM library\n  (e.g., ``canonicalize``, ``signature_algorithm``).\n\nReturns the message instance (for chaining).\n\nExample::\n\n    import emails\n\n    msg = emails.Message(\n        html=\"<p>Signed message</p>\",\n        mail_from=(\"Sender\", \"sender@example.com\"),\n        subject=\"DKIM Test\"\n    )\n    msg.dkim(\n        key=open(\"private.pem\").read(),\n        domain=\"example.com\",\n        selector=\"default\"\n    )\n    msg.send(to=\"recipient@example.com\", smtp={\"host\": \"localhost\"})\n\nThe signature is automatically applied when the message is serialized\n(via :meth:`~Message.as_string`, :meth:`~Message.as_bytes`, or :meth:`~Message.send`).\n\n\nExceptions\n----------\n\n.. exception:: emails.HTTPLoaderError\n\n   Raised when loading content from a URL fails (e.g., HTTP error, connection timeout).\n\n.. exception:: emails.BadHeaderError\n\n   Raised when an email header contains invalid characters (such as newlines\n   or carriage returns).\n\n.. exception:: emails.IncompleteMessage\n\n   Raised when attempting to send a message that lacks required content\n   (no HTML and no text body).\n\nSee also the `Loader Exceptions`_ section for loader-specific exceptions:\n``LoadError``, ``IndexFileNotFound``, ``InvalidHtmlFile``.\n\n\nUtilities\n---------\n\n.. class:: emails.utils.MessageID(domain=None, idstring=None)\n\n   Generator for RFC 2822 compliant Message-ID values.\n\n   :param domain: Domain part of the Message-ID. Defaults to the machine's FQDN.\n   :param idstring: Optional additional string to strengthen uniqueness.\n\n   The instance is callable — each call generates a new unique Message-ID.\n\n   Example::\n\n       from emails.utils import MessageID\n\n       # Auto-generate a new Message-ID for each send\n       msg = emails.Message(\n           message_id=MessageID(domain=\"example.com\"),\n           html=\"<p>Hello</p>\",\n           mail_from=\"sender@example.com\"\n       )\n\n.. function:: emails.html(\\*\\*kwargs)\n\n   Convenience function that creates and returns a :class:`Message` instance.\n   Accepts all the same parameters as the :class:`Message` constructor.\n\n   Example::\n\n       msg = emails.html(\n           html=\"<p>Hello!</p>\",\n           subject=\"Test\",\n           mail_from=\"sender@example.com\"\n       )\n"
  },
  {
    "path": "docs/conf.py",
    "content": "import sys\nimport os\n\nsys.path.append(os.path.abspath('..'))\n\n# -- General configuration ------------------------------------------------\n\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.doctest',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.coverage',\n    'sphinx.ext.ifconfig',\n    'sphinx.ext.viewcode',\n    'sphinx_togglebutton',\n]\n\ntemplates_path = ['_templates']\nsource_suffix = '.rst'\nmaster_doc = 'index'\n\nproject = 'python-emails'\ncopyright = '2015-2026, Sergey Lavrinenko'\n\nfrom emails import __version__ as _emails_version\nversion = '.'.join(_emails_version.split('.')[:2])\nrelease = _emails_version\n\nexclude_patterns = ['_build', 'examples.rst']\npygments_style = 'sphinx'\n\n# -- Options for HTML output ----------------------------------------------\n\nhtml_theme = 'furo'\n\nhtml_theme_options = {\n    \"source_repository\": \"https://github.com/lavr/python-emails\",\n    \"source_branch\": \"master\",\n    \"source_directory\": \"docs/\",\n    \"navigation_with_keys\": True,\n}\n\nhtml_title = f\"python-emails {release}\"\n\nhtml_static_path = ['_static']\n\nhtmlhelp_basename = 'python-emailsdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {}\n\nlatex_documents = [\n    ('index', 'python-emails.tex', 'python-emails Documentation',\n     'Sergey Lavrinenko', 'manual'),\n]\n\n# -- Options for manual page output ---------------------------------------\n\nman_pages = [\n    ('index', 'python-emails', 'python-emails Documentation',\n     ['Sergey Lavrinenko'], 1)\n]\n\n# -- Options for Texinfo output -------------------------------------------\n\ntexinfo_documents = [\n    ('index', 'python-emails', 'python-emails Documentation',\n     'Sergey Lavrinenko', 'python-emails',\n     'Modern email handling in python.',\n     'Miscellaneous'),\n]\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {'python': ('https://docs.python.org/3', None)}\n"
  },
  {
    "path": "docs/examples.rst",
    "content": "Features\n--------\n\n-  HTML-email message abstraction\n-  Method to transform html body:\n\n   - css inlining (using peterbe's premailer)\n   - image inlining\n-  DKIM signature\n-  Message loaders\n-  Send directly or via django email backend\n\n\nExamples\n--------\n\nCreate message:\n\n.. code-block:: python\n\n    import emails\n    message = emails.html(html=open('letter.html'),\n                          subject='Friday party',\n                          mail_from=('Company Team', 'contact@mycompany.com'))\n\n\nAttach files or inline images:\n\n.. code-block:: python\n\n    message.attach(data=open('event.ics', 'rb'), filename='Event.ics')\n    message.attach(data=open('image.png', 'rb'), filename='image.png',\n                   content_disposition='inline')\n\nUse templates (requires ``pip install \"emails[jinja]\"``):\n\n.. code-block:: python\n\n    from emails.template import JinjaTemplate as T\n\n    message = emails.html(subject=T('Payment Receipt No.{{ billno }}'),\n                          html=T('<p>Dear {{ name }}! This is a receipt...'),\n                          mail_from=('ABC', 'robot@mycompany.com'))\n\n    message.send(to=('John Brown', 'jbrown@gmail.com'),\n                 render={'name': 'John Brown', 'billno': '141051906163'})\n\n\n\nAdd DKIM signature:\n\n.. code-block:: python\n\n    message.dkim(key=open('my.key'), domain='mycompany.com', selector='newsletter')\n\nGenerate email.message or rfc822 string:\n\n.. code-block:: python\n\n    m = message.as_message()\n    s = message.as_string()\n\n\nSend and get response from smtp server:\n\n.. code-block:: python\n\n    r = message.send(to=('John Brown', 'jbrown@gmail.com'),\n                     render={'name': 'John'},\n                     smtp={'host':'smtp.mycompany.com', 'port': 465, 'ssl': True, 'user': 'john', 'password': '***'})\n    assert r.status_code == 250\n\n\nDjango\n------\n\nDjangoMessage helper sends via django configured email backend:\n\n.. code-block:: python\n\n    from emails.django import DjangoMessage as Message\n    message = Message(...)\n    message.send(mail_to=('John Brown', 'jbrown@gmail.com'),\n                 context={'name': 'John'})\n\nFlask\n-----\n\nFor flask integration take a look at `flask-emails <https://github.com/lavr/flask-emails>`_\n"
  },
  {
    "path": "docs/faq.rst",
    "content": "FAQ\n===\n\nFrequently asked questions about ``python-emails``.\n\n\nHow do I send through Gmail / Yandex / other providers?\n-------------------------------------------------------\n\nAll SMTP providers follow the same pattern — pass the provider's SMTP\nhost, port, and credentials in the ``smtp`` dict:\n\n.. code-block:: python\n\n    response = message.send(\n        to=\"recipient@example.com\",\n        smtp={\n            \"host\": \"<provider SMTP host>\",\n            \"port\": 587,\n            \"tls\": True,\n            \"user\": \"your-email@example.com\",\n            \"password\": \"your-password-or-app-password\"\n        }\n    )\n\nCommon SMTP settings:\n\n.. list-table::\n   :header-rows: 1\n   :widths: 20 30 10 15\n\n   * - Provider\n     - Host\n     - Port\n     - Encryption\n   * - Gmail\n     - ``smtp.gmail.com``\n     - 587\n     - ``tls=True``\n   * - Yandex\n     - ``smtp.yandex.ru``\n     - 465\n     - ``ssl=True``\n   * - Outlook / Hotmail\n     - ``smtp-mail.outlook.com``\n     - 587\n     - ``tls=True``\n   * - Yahoo Mail\n     - ``smtp.mail.yahoo.com``\n     - 465\n     - ``ssl=True``\n\n.. note::\n\n   Most providers require an **app password** instead of your regular\n   account password. Consult the provider's documentation:\n\n   - `Gmail: Sign in with app passwords <https://support.google.com/accounts/answer/185833>`_\n   - `Yandex: App passwords <https://yandex.com/support/id/authorization/app-passwords.html>`_\n   - `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>`_\n   - `Yahoo: App passwords <https://help.yahoo.com/kb/generate-manage-third-party-passwords-sln15241.html>`_\n\n   Provider settings and authentication requirements change over time.\n   Always refer to the official documentation for up-to-date instructions.\n\n\nHow do I attach a PDF or Excel file?\n-------------------------------------\n\nUse :meth:`~emails.Message.attach` with the file's data and filename.\nThe MIME type is auto-detected from the filename extension:\n\n.. code-block:: python\n\n    # Attach a PDF\n    message.attach(filename=\"report.pdf\", data=open(\"report.pdf\", \"rb\"))\n\n    # Attach an Excel file\n    message.attach(filename=\"data.xlsx\", data=open(\"data.xlsx\", \"rb\"))\n\n    # Attach with an explicit MIME type\n    message.attach(\n        filename=\"archive.7z\",\n        data=open(\"archive.7z\", \"rb\"),\n        mime_type=\"application/x-7z-compressed\"\n    )\n\nYou can also attach in-memory data:\n\n.. code-block:: python\n\n    import io\n\n    csv_data = \"name,score\\nAlice,95\\nBob,87\\n\"\n    message.attach(\n        filename=\"scores.csv\",\n        data=io.BytesIO(csv_data.encode(\"utf-8\"))\n    )\n\n\nHow is python-emails different from smtplib + email.mime?\n---------------------------------------------------------\n\n``python-emails`` is built on top of the standard library's ``email`` and\n``smtplib`` modules. The difference is the level of abstraction.\n\nWith ``python-emails``:\n\n.. code-block:: python\n\n    import emails\n    from emails.template import JinjaTemplate as T\n\n    message = emails.html(\n        subject=T(\"Passed: {{ project_name }}#{{ build_id }}\"),\n        html=T(\"<html><p>Build passed: {{ project_name }} \"\n               \"<img src='cid:icon.png'> ...</p></html>\"),\n        text=T(\"Build passed: {{ project_name }} ...\"),\n        mail_from=(\"CI\", \"ci@mycompany.com\")\n    )\n    message.attach(filename=\"icon.png\", data=open(\"icon.png\", \"rb\"),\n                   content_disposition=\"inline\")\n\n    message.send(\n        to=\"somebody@mycompany.com\",\n        render={\"project_name\": \"user/project1\", \"build_id\": 121},\n        smtp={\"host\": \"smtp.mycompany.com\", \"port\": 587, \"tls\": True,\n              \"user\": \"ci\", \"password\": \"secret\"}\n    )\n\nThe same message with the standard library alone:\n\n.. container:: toggle\n\n    .. code-block:: python\n\n        import os\n        import smtplib\n        from email.utils import formataddr, formatdate, COMMASPACE\n        from email.header import Header\n        from email import encoders\n        from email.mime.multipart import MIMEMultipart\n        from email.mime.base import MIMEBase\n        from email.mime.text import MIMEText\n        from email.mime.image import MIMEImage\n        import jinja2\n\n        sender_name, sender_email = \"CI\", \"ci@mycompany.com\"\n        recipient_addr = [\"somebody@mycompany.com\"]\n\n        j = jinja2.Environment()\n        ctx = {\"project_name\": \"user/project1\", \"build_id\": 121}\n        html = j.from_string(\n            \"<html><p>Build passed: {{ project_name }} \"\n            \"<img src='cid:icon.png'> ...</p></html>\"\n        ).render(**ctx)\n        text = j.from_string(\"Build passed: {{ project_name }} ...\").render(**ctx)\n        subject = j.from_string(\n            \"Passed: {{ project_name }}#{{ build_id }}\"\n        ).render(**ctx)\n\n        encoded_name = Header(sender_name, \"utf-8\").encode()\n        msg_root = MIMEMultipart(\"mixed\")\n        msg_root[\"Date\"] = formatdate(localtime=True)\n        msg_root[\"From\"] = formataddr((encoded_name, sender_email))\n        msg_root[\"To\"] = COMMASPACE.join(recipient_addr)\n        msg_root[\"Subject\"] = Header(subject, \"utf-8\")\n        msg_root.preamble = \"This is a multi-part message in MIME format.\"\n\n        msg_related = MIMEMultipart(\"related\")\n        msg_root.attach(msg_related)\n        msg_alternative = MIMEMultipart(\"alternative\")\n        msg_related.attach(msg_alternative)\n\n        msg_text = MIMEText(text.encode(\"utf-8\"), \"plain\", \"utf-8\")\n        msg_alternative.attach(msg_text)\n        msg_html = MIMEText(html.encode(\"utf-8\"), \"html\", \"utf-8\")\n        msg_alternative.attach(msg_html)\n\n        with open(\"icon.png\", \"rb\") as fp:\n            msg_image = MIMEImage(fp.read())\n            msg_image.add_header(\"Content-ID\", \"<icon.png>\")\n            msg_related.attach(msg_image)\n\n        mail_server = smtplib.SMTP(\"smtp.mycompany.com\", 587)\n        mail_server.ehlo()\n        try:\n            mail_server.starttls()\n            mail_server.ehlo()\n        except smtplib.SMTPException as e:\n            print(e)\n        mail_server.login(\"ci\", \"secret\")\n        mail_server.send_message(msg_root)\n        mail_server.quit()\n\nThe standard library version requires:\n\n- Manual MIME tree construction (``MIMEMultipart`` nesting of ``mixed``,\n  ``related``, and ``alternative`` parts)\n- Explicit header encoding with ``Header``\n- Manual ``Content-ID`` management for inline images\n- Separate template rendering before message assembly\n- Direct SMTP session management (``ehlo``, ``starttls``, ``login``,\n  ``quit``)\n\n``python-emails`` handles all of this internally.\n\n\nHow is python-emails different from django.core.mail?\n-----------------------------------------------------\n\n``django.core.mail`` is Django's built-in email module. It works well\nwithin Django but has several limitations compared to ``python-emails``:\n\n- **No HTML transformations** — ``django.core.mail`` sends HTML as-is.\n  ``python-emails`` can inline CSS, embed images, and clean up unsafe\n  tags via :meth:`~emails.Message.transform`.\n- **No template integration** — with ``django.core.mail`` you render\n  templates manually before passing HTML to the message.\n  ``python-emails`` accepts template objects directly in ``html``,\n  ``text``, and ``subject``.\n- **No loaders** — ``python-emails`` can create messages from URLs, ZIP\n  archives, directories, and ``.eml`` files.\n- **No DKIM** — ``python-emails`` supports DKIM signing out of the box.\n- **Django-only** — ``django.core.mail`` requires a Django project.\n  ``python-emails`` works in any Python project.\n\nIf you are in a Django project and want to use ``python-emails``,\nthe :class:`~emails.django.DjangoMessage` class integrates with Django's\nemail backend:\n\n.. code-block:: python\n\n    from emails.django import DjangoMessage\n\n    message = DjangoMessage(\n        html=\"<p>Hello {{ name }}!</p>\",\n        subject=\"Welcome\",\n        mail_from=\"noreply@example.com\"\n    )\n    message.send(to=\"user@example.com\", context={\"name\": \"Alice\"})\n\nSee the :doc:`Django Integration <advanced>` section for more details.\n\n\nHow do I debug email sending?\n-----------------------------\n\nThere are two levels of debugging: SMTP protocol tracing and Python\nlogging.\n\n\nSMTP Protocol Trace\n~~~~~~~~~~~~~~~~~~~\n\nSet ``debug=1`` in the ``smtp`` dict to print the full SMTP conversation\nto stdout:\n\n.. code-block:: python\n\n    response = message.send(\n        to=\"user@example.com\",\n        smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n              \"user\": \"me\", \"password\": \"secret\", \"debug\": 1}\n    )\n\nThis outputs every command and response exchanged with the SMTP server,\nwhich is useful for diagnosing authentication failures, TLS issues, and\nrejected recipients.\n\n\nPython Logging\n~~~~~~~~~~~~~~\n\nThe library uses Python's standard ``logging`` module. Enable it to see\nconnection events and retries:\n\n.. code-block:: python\n\n    import logging\n\n    logging.basicConfig(level=logging.DEBUG)\n\n    # Or enable only the emails loggers:\n    logging.getLogger(\"emails.backend.smtp.backend\").setLevel(logging.DEBUG)\n    logging.getLogger(\"emails.backend.smtp.client\").setLevel(logging.DEBUG)\n\nLogger names used by the library:\n\n- ``emails.backend.smtp.backend`` — connection management, retries\n- ``emails.backend.smtp.client`` — SMTP client operations\n\n\nInspecting the Message\n~~~~~~~~~~~~~~~~~~~~~~\n\nBefore sending, you can inspect the raw RFC 822 output:\n\n.. code-block:: python\n\n    print(message.as_string())\n\nThis shows the full MIME structure, headers, and encoded content —\nuseful for verifying that attachments, inline images, and headers are\ncorrect.\n\n\nChecking the Response\n~~~~~~~~~~~~~~~~~~~~~\n\nAfter sending, inspect the :class:`SMTPResponse` object:\n\n.. code-block:: python\n\n    response = message.send(to=\"user@example.com\", smtp={...})\n\n    print(f\"Status: {response.status_code}\")\n    print(f\"Text: {response.status_text}\")\n    print(f\"Success: {response.success}\")\n\n    if response.error:\n        print(f\"Error: {response.error}\")\n\n    if response.refused_recipients:\n        for addr, (code, reason) in response.refused_recipients.items():\n            print(f\"Refused {addr}: {code} {reason}\")\n"
  },
  {
    "path": "docs/howtohelp.rst",
    "content": "How to Help\n===========\n\nContributions are welcome! Here is how you can help:\n\n1. `Open an issue <https://github.com/lavr/python-emails/issues>`_ to report a bug or suggest a feature.\n2. Fork the repository on GitHub and start making your changes on a new branch.\n3. Write a test which shows that the bug was fixed.\n4. Send a pull request. Make sure to add yourself to `AUTHORS <https://github.com/lavr/python-emails/blob/master/README.rst>`_.\n"
  },
  {
    "path": "docs/index.rst",
    "content": "python-emails\n=============\n\n.. module:: emails\n\n|pypi| |python| |license|\n\n.. |pypi| image:: https://img.shields.io/pypi/v/emails.svg\n   :target: https://pypi.org/project/emails/\n   :alt: PyPI version\n\n.. |python| image:: https://img.shields.io/pypi/pyversions/emails.svg\n   :target: https://pypi.org/project/emails/\n   :alt: Python versions\n\n.. |license| image:: https://img.shields.io/pypi/l/emails.svg\n   :target: https://github.com/lavr/python-emails/blob/master/LICENSE\n   :alt: License\n\nModern email handling in python. Build, transform, and send emails with a\nclean, intuitive API.\n\n.. code-block:: python\n\n    import emails\n\n    message = emails.html(\n        subject=\"Hi from python-emails!\",\n        html=\"<html><p>Hello, <strong>World!</strong></p></html>\",\n        mail_from=(\"Alice\", \"alice@example.com\"),\n    )\n    response = message.send(\n        to=\"bob@example.com\",\n        smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True},\n    )\n    assert response.status_code == 250\n\n\n.. rubric:: Features\n\n- Build HTML and plain-text emails with a simple API\n- CSS inlining, image embedding, and HTML cleanup via built-in transformations\n- Jinja2, Mako, and string templates for dynamic content\n- Inline images and file attachments\n- DKIM signing\n- Load messages from URLs, HTML files, directories, ZIP archives, or RFC 822 files\n- Django integration via ``DjangoMessage``\n- SMTP sending with SSL/TLS support\n- Async sending via ``aiosmtplib``\n\n\n.. toctree::\n   :maxdepth: 2\n\n   quickstart\n   transformations\n   advanced\n   api\n   faq\n   install\n   howtohelp\n   links\n"
  },
  {
    "path": "docs/install.rst",
    "content": "Install\n=======\n\nInstall from pypi:\n\n.. code-block:: bash\n\n    $ pip install emails\n\nThis installs the lightweight core for building and sending email messages.\n\nTo use HTML transformation features (CSS inlining, image embedding, loading from URL/file):\n\n.. code-block:: bash\n\n    $ pip install \"emails[html]\"\n\nTo use Jinja2 templates (the ``T()`` shortcut):\n\n.. code-block:: bash\n\n    $ pip install \"emails[jinja]\"\n\nTo use async sending (``send_async()``):\n\n.. code-block:: bash\n\n    $ pip install \"emails[async]\"\n"
  },
  {
    "path": "docs/links.rst",
    "content": "See also\n========\n\nAlternatives\n------------\n\nThere are several Python libraries for sending email, each with a different focus:\n\n- **smtplib + email** (standard library) — built into Python, provides low-level SMTP\n  transport and RFC-compliant message construction. Full control, but requires manual\n  MIME assembly for HTML emails with attachments. python-emails builds on top of these\n  modules and adds a higher-level API with HTML transformations, template support, and\n  loaders.\n\n- `yagmail <https://github.com/kootenpv/yagmail>`_ — a friendly Gmail/SMTP client that\n  auto-detects content types and simplifies sending. Supports OAuth2 for Gmail and\n  optional DKIM signing. A good choice when you need a quick way to send emails with\n  minimal setup.\n\n- `red-mail <https://github.com/Miksus/red-mail>`_ — advanced email sending with\n  built-in Jinja2 templates, prettified HTML tables, and embedded images. A good fit\n  if you need to send data-driven reports.\n\n- `envelope <https://github.com/CZ-NIC/envelope>`_ — an all-in-one library with GPG\n  and S/MIME encryption, a fluent Python API, and a CLI interface. The right choice when\n  email encryption or signing is a requirement.\n\npython-emails focuses on **HTML email as a first-class citizen**: loading HTML from\nURLs, files, ZIP archives, or directories, automatic CSS inlining and image embedding\nvia built-in transformations, and multiple template engines (Jinja2, Mako, string\ntemplates). It also provides DKIM signing and Django integration out of the box.\n\n\nAcknowledgements\n----------------\n\npython-emails uses `premailer <https://github.com/peterbe/premailer>`_ for CSS inlining\n— converting ``<style>`` blocks into inline ``style`` attributes for maximum email client\ncompatibility.\n"
  },
  {
    "path": "docs/quickstart.rst",
    "content": "Quickstart\n==========\n\n``python-emails`` is a library for composing and sending email messages\nin Python. It provides a clean, high-level API over the standard library's\n``email`` and ``smtplib`` modules.\n\nWith ``smtplib`` and ``email.mime``, sending an HTML email with an attachment\nrequires assembling MIME parts manually, encoding headers, handling character\nsets, and managing the SMTP connection — often 30+ lines of boilerplate for\na simple message. ``python-emails`` reduces that to a few lines:\n\n.. code-block:: python\n\n    import emails\n\n    message = emails.html(\n        html=\"<p>Hello, World!</p>\",\n        subject=\"My first email\",\n        mail_from=(\"Me\", \"me@example.com\")\n    )\n    response = message.send(to=\"you@example.com\",\n                            smtp={\"host\": \"smtp.example.com\", \"port\": 587,\n                                  \"tls\": True, \"user\": \"me\", \"password\": \"secret\"})\n\nThe library handles MIME structure, character encoding, inline images,\nCSS inlining, DKIM signing, and template rendering — things that are tedious\nto do correctly with the standard library.\n\n\nCreating a Message\n------------------\n\nThe simplest way to create a message is :func:`emails.html`:\n\n.. code-block:: python\n\n    import emails\n\n    message = emails.html(\n        html=\"<h1>Friday party!</h1><p>You are invited.</p>\",\n        subject=\"Friday party\",\n        mail_from=(\"Company Team\", \"contact@mycompany.com\")\n    )\n\n:func:`emails.html` is a shortcut for :class:`~emails.Message` — both accept\nthe same parameters:\n\n- ``html`` — HTML body content (string or file-like object)\n- ``text`` — plain text alternative\n- ``subject`` — email subject line\n- ``mail_from`` — sender address, as a string ``\"user@example.com\"`` or\n  a tuple ``(\"Display Name\", \"user@example.com\")``\n- ``mail_to`` — recipient(s), same format as ``mail_from``; also accepts a list\n\nYou can also set ``cc``, ``bcc``, ``reply_to``, ``headers``, ``charset``,\nand other parameters. See the :doc:`API Reference <api>` for full details.\n\nIf you have HTML in a file:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=open(\"letter.html\"),\n        subject=\"Newsletter\",\n        mail_from=\"newsletter@example.com\"\n    )\n\n\nSending a Message\n-----------------\n\nCall :meth:`~emails.Message.send` with an ``smtp`` dict describing\nyour SMTP server:\n\n.. code-block:: python\n\n    response = message.send(\n        to=\"recipient@example.com\",\n        smtp={\n            \"host\": \"smtp.example.com\",\n            \"port\": 587,\n            \"tls\": True,\n            \"user\": \"me@example.com\",\n            \"password\": \"secret\"\n        }\n    )\n\nThe ``smtp`` dict supports these keys:\n\n- ``host`` — SMTP server hostname (default: ``\"localhost\"``)\n- ``port`` — server port (default: ``25``)\n- ``ssl`` — use SSL/TLS connection (for port 465)\n- ``tls`` — use STARTTLS (for port 587)\n- ``user`` — username for authentication\n- ``password`` — password for authentication\n- ``timeout`` — connection timeout in seconds (default: ``5``)\n\n:meth:`~emails.Message.send` returns an :class:`SMTPResponse` object.\nCheck ``status_code`` to verify the message was accepted:\n\n.. code-block:: python\n\n    if response.status_code == 250:\n        print(\"Message sent successfully\")\n    else:\n        print(f\"Send failed: {response.status_code}\")\n\n\nAttachments\n-----------\n\nUse :meth:`~emails.Message.attach` to add files to a message:\n\n.. code-block:: python\n\n    message.attach(filename=\"report.pdf\", data=open(\"report.pdf\", \"rb\"))\n    message.attach(filename=\"data.csv\", data=open(\"data.csv\", \"rb\"))\n\nEach attachment gets ``content_disposition='attachment'`` by default,\nwhich means the file appears as a downloadable attachment in the recipient's\nemail client.\n\nYou can specify a MIME type explicitly:\n\n.. code-block:: python\n\n    message.attach(\n        filename=\"event.ics\",\n        data=open(\"event.ics\", \"rb\"),\n        mime_type=\"text/calendar\"\n    )\n\nIf ``mime_type`` is not specified, it is auto-detected from the filename.\n\n\nInline Images\n-------------\n\nInline images are embedded directly in the HTML body rather than shown as\nattachments. They use the ``cid:`` (Content-ID) URI scheme to reference\nembedded content.\n\nTo use an inline image:\n\n1. Reference it in your HTML with ``cid:filename``\n2. Attach it with ``content_disposition=\"inline\"``\n\n.. code-block:: python\n\n    message = emails.html(\n        html='<p>Hello! <img src=\"cid:logo.png\"></p>',\n        subject=\"With inline image\",\n        mail_from=\"sender@example.com\"\n    )\n    message.attach(\n        filename=\"logo.png\",\n        data=open(\"logo.png\", \"rb\"),\n        content_disposition=\"inline\"\n    )\n\nThe ``cid:logo.png`` in the HTML ``src`` attribute tells the email client\nto display the attached file named ``logo.png`` inline at that position,\nrather than as a separate attachment.\n\n\nTemplates\n---------\n\nFor emails with dynamic content, use template classes instead of plain strings.\nThe most common choice is :class:`~emails.template.JinjaTemplate`, which uses\n`Jinja2 <https://jinja.palletsprojects.com/>`_ syntax:\n\n.. code-block:: python\n\n    from emails.template import JinjaTemplate as T\n\n    message = emails.html(\n        subject=T(\"Payment Receipt No.{{ bill_no }}\"),\n        html=T(\"<p>Dear {{ name }},</p><p>Your payment of ${{ amount }} was received.</p>\"),\n        mail_from=(\"Billing\", \"billing@mycompany.com\")\n    )\n\nPass template variables via the ``render`` parameter of :meth:`~emails.Message.send`:\n\n.. code-block:: python\n\n    message.send(\n        to=\"customer@example.com\",\n        render={\"name\": \"Alice\", \"bill_no\": \"12345\", \"amount\": \"99.00\"},\n        smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n              \"user\": \"billing\", \"password\": \"secret\"}\n    )\n\nTemplates work in ``html``, ``text``, and ``subject`` — all three are rendered\nwith the same variables.\n\nJinja2 templates require the ``jinja2`` package. Install it with::\n\n    pip install \"emails[jinja]\"\n\nTwo other template backends are available:\n\n- :class:`~emails.template.StringTemplate` — uses Python's\n  :class:`string.Template` syntax (``$variable``)\n- :class:`~emails.template.MakoTemplate` — uses\n  `Mako <https://www.makotemplates.org/>`_ syntax (requires the ``mako`` package)\n\n\nDKIM Signing\n------------\n\nDKIM (DomainKeys Identified Mail) lets the recipient verify that an email\nwas authorized by the domain owner. This improves deliverability and reduces\nthe chance of messages being marked as spam.\n\nTo sign a message, call :meth:`~emails.Message.dkim` with your private key,\ndomain, and selector:\n\n.. code-block:: python\n\n    message.dkim(\n        key=open(\"private.pem\", \"rb\"),\n        domain=\"mycompany.com\",\n        selector=\"default\"\n    )\n\nThe signature is applied automatically when the message is sent or serialized.\nThe method returns the message instance, so you can chain it:\n\n.. code-block:: python\n\n    message = emails.html(\n        html=\"<p>Signed message</p>\",\n        mail_from=\"sender@mycompany.com\",\n        subject=\"DKIM Test\"\n    ).dkim(key=open(\"private.pem\", \"rb\"), domain=\"mycompany.com\", selector=\"default\")\n\nDKIM requires a private key in PEM format and a corresponding DNS TXT record\non your domain. Consult your DNS provider's documentation for setting up\nthe DNS record.\n\n\nGenerating Without Sending\n---------------------------\n\nSometimes you need the raw email content without actually sending it —\nfor example, to store it, pass it to another system, or inspect it.\n\n:meth:`~emails.Message.as_string` returns the full RFC 822 message as a string:\n\n.. code-block:: python\n\n    raw = message.as_string()\n    print(raw)\n\n:meth:`~emails.Message.as_message` returns a standard library\n:class:`email.message.Message` object, which you can inspect or manipulate:\n\n.. code-block:: python\n\n    msg = message.as_message()\n    print(msg[\"Subject\"])\n    print(msg[\"From\"])\n\nThere is also :meth:`~emails.Message.as_bytes` if you need the message\nas bytes.\n\nIf DKIM signing is configured, the signature is included in the output\nof all three methods.\n\n\nError Handling\n--------------\n\n:meth:`~emails.Message.send` returns an :class:`SMTPResponse` object.\nIt never raises an exception for SMTP errors by default — instead, error\ninformation is available on the response:\n\n.. code-block:: python\n\n    response = message.send(\n        to=\"recipient@example.com\",\n        smtp={\"host\": \"smtp.example.com\", \"port\": 587, \"tls\": True,\n              \"user\": \"me\", \"password\": \"secret\"}\n    )\n\n    if response.success:\n        print(\"Sent!\")\n    else:\n        print(f\"Failed with status {response.status_code}: {response.status_text}\")\n\n        # Check for connection/auth errors\n        if response.error:\n            print(f\"Error: {response.error}\")\n\n        # Check for rejected recipients\n        if response.refused_recipients:\n            for addr, (code, reason) in response.refused_recipients.items():\n                print(f\"  Refused {addr}: {code} {reason}\")\n\nKey attributes of :class:`SMTPResponse`:\n\n- ``success`` — ``True`` if the message was accepted (status code 250)\n- ``status_code`` — the SMTP response code (``250``, ``550``, etc.), or ``None`` on connection failure\n- ``status_text`` — the SMTP server's response text\n- ``error`` — the exception object if a connection or protocol error occurred\n- ``refused_recipients`` — a dict of recipients rejected by the server\n- ``last_command`` — the last SMTP command that was attempted (``'mail'``, ``'rcpt'``, ``'data'``)\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "furo\nsphinx-togglebutton\n"
  },
  {
    "path": "docs/transformations.rst",
    "content": "HTML transformer\n================\n\n.. testsetup:: *\n\n    import emails\n    import io\n\nMessage HTML body usually should be modified before sent.\n\nBase transformations, such as css inlining can be made by `Message.transform` method:\n\n.. doctest::\n\n    >>> message = emails.Message(html=\"<style>h1{color:red}</style><h1>Hello world!</h1>\")\n    >>> message.transform()\n    >>> message.html  # doctest: +ELLIPSIS\n    '<html><head>...</head><body><h1 style=\"color:red\">Hello world!</h1></body></html>'\n\n`Message.transform` can take some arguments with speaken names `css_inline`, `remove_unsafe_tags`,\n`make_links_absolute`, `set_content_type_meta`, `update_stylesheet`, `images_inline`.\n\nMore specific transformation can be made via `transformer` property.\n\nExample of custom link transformations:\n\n.. doctest::\n\n    >>> message = emails.Message(html=\"<img src='promo.png'>\")\n    >>> message.transformer.apply_to_images(func=lambda src, **kw: 'http://mycompany.tld/images/'+src)\n    >>> message.transformer.save()\n    >>> message.html\n    '<html><body><img src=\"http://mycompany.tld/images/promo.png\"/></body></html>'\n\nExample of customized making images inline:\n\n.. doctest::\n\n    >>> message = emails.Message(html=\"<img src='promo.png'>\")\n    >>> message.attach(filename='promo.png', data=io.BytesIO(b'PNG_DATA'))\n    >>> message.attachments['promo.png'].is_inline = True\n    >>> _ = message.transformer.synchronize_inline_images()\n    >>> message.transformer.save()\n    >>> message.html\n    '<html><body><img src=\"cid:promo.png\"/></body></html>'\n\n\nLoaders\n-------\n\npython-emails ships with couple of loaders.\n\nLoad message from url:\n\n.. code-block:: python\n\n    import emails.loader\n    message = emails.loader.from_url(url=\"http://xxx.github.io/newsletter/2015-08-14/index.html\")\n\n\nLoad from zipfile or directory:\n\n.. code-block:: python\n\n    message = emails.loader.from_zip(open('design_pack.zip', 'rb'))\n    message = emails.loader.from_directory('/home/user/design_pack')\n\nZipfile and directory loaders require at least one html file (with \"html\" extension).\n\nLoad message from `.eml` file (experimental):\n\n.. code-block:: python\n\n    message = emails.loader.from_rfc822(open('message.eml').read())\n"
  },
  {
    "path": "emails/__init__.py",
    "content": "\"\"\"\npython-emails\n~~~~~~~~~~~~~\n\nModern python library for email.\n\nBuild message:\n\n   >>> import emails\n   >>> message = emails.html(html=\"<p>Hi!<br>Here is your receipt...\",\n                          subject=\"Your receipt No. 567098123\",\n                          mail_from=('Some Store', 'store@somestore.com'))\n   >>> message.attach(data=open('bill.pdf'), filename='bill.pdf')\n\nsend message and get response from smtp server:\n\n   >>> r = message.send(to='s@lavr.me', smtp={'host': 'aspmx.l.google.com', 'timeout': 5})\n   >>> assert r.status_code == 250\n\nand more:\n\n * DKIM signature\n * Render body from template\n * Flask extension and Django integration\n * Message body transformation methods\n * Load message from url or from file\n\n\nLinks\n`````\n\n* `documentation <https://python-emails.readthedocs.io/>`_\n* `source code <https://github.com/lavr/python-emails>`_\n\n\"\"\"\n\n\n__title__ = 'emails'\n__version__ = '1.1.1'\n__author__ = 'Sergey Lavrinenko'\n__license__ = 'Apache 2.0'\n__copyright__ = 'Copyright 2013-2026 Sergey Lavrinenko'\n\nUSER_AGENT: str = 'python-emails/%s' % __version__\n\nfrom .message import Message, html\nfrom .utils import MessageID\nfrom .exc import HTTPLoaderError, BadHeaderError, IncompleteMessage\n\n\n"
  },
  {
    "path": "emails/backend/__init__.py",
    "content": "from .factory import ObjectFactory\nfrom .smtp import SMTPBackend\n"
  },
  {
    "path": "emails/backend/factory.py",
    "content": "\ndef simple_dict2str(d):\n    # Simple dict serializer\n    return \";\".join([\"%s=%s\" % (k, v) for (k, v) in d.items()])\n\n_serializer = simple_dict2str\n\nclass ObjectFactory:\n\n    \"\"\"\n    Get object from cache or create new object.\n    \"\"\"\n\n    def __init__(self, cls):\n        self.cls = cls\n        self.pool = {}\n\n    def __getitem__(self, k):\n        if not isinstance(k, dict):\n            raise ValueError(\"item must be dict, not %s\" % type(k))\n        cache_key = _serializer(k)\n        obj = self.pool.get(cache_key, None)\n        if obj is None:\n            obj = self.cls(**k)\n            self.pool[cache_key] = obj\n        return obj\n\n    def invalidate(self, k):\n        cache_key = _serializer(k)\n        if cache_key in self.pool:\n            del self.pool[cache_key]\n        return self[k]"
  },
  {
    "path": "emails/backend/inmemory/__init__.py",
    "content": "\n__all__ = ['InMemoryBackend', ]\n\nimport logging\n\n\nclass InMemoryBackend(object):\n\n    \"\"\"\n    InMemoryBackend store message in memory for testing purposes.\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        self.kwargs = kwargs\n        self.messages = {}\n\n    def sendmail(self, from_addr, to_addrs, msg, **kwargs):\n\n        logging.debug('InMemoryBackend.sendmail(%s, %s, %r, %s)', from_addr, to_addrs, msg, kwargs)\n\n        if not to_addrs:\n            return None\n\n        if not isinstance(to_addrs, (list, tuple)):\n            to_addrs = [to_addrs, ]\n\n        for addr in to_addrs:\n            data = dict(from_addr=from_addr,\n                        message=msg.as_string(),\n                        source_message=msg,\n                        **kwargs)\n            self.messages.setdefault(addr.lower(), []).append(data)\n\n        return True\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        pass"
  },
  {
    "path": "emails/backend/response.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\n\nclass Response:\n\n    def __init__(self, exception: Exception | None = None, backend: Any = None) -> None:\n        self.backend = backend\n        self.set_exception(exception)\n        self.from_addr: str | None = None\n        self.to_addrs: list[str] | None = None\n        self._finished: bool = False\n\n    def set_exception(self, exc: Exception | None) -> None:\n        self._exc = exc\n\n    def raise_if_needed(self) -> None:\n        if self._exc:\n            raise self._exc\n\n    @property\n    def error(self) -> Exception | None:\n        return self._exc\n\n    @property\n    def success(self) -> bool:\n        return self._finished\n\n\nclass SMTPResponse(Response):\n\n    def __init__(self, exception: Exception | None = None, backend: Any = None) -> None:\n\n        super(SMTPResponse, self).__init__(exception=exception, backend=backend)\n\n        self.responses: list[list] = []\n\n        self.esmtp_opts: list[str] | None = None\n        self.rcpt_options: list[str] | None = None\n\n        self.status_code: int | None = None\n        self.status_text: bytes | None = None\n        self.last_command: str | None = None\n        self.refused_recipients: dict[str, tuple[int, bytes]] = {}\n\n    def set_status(self, command: str, code: int, text: bytes, **kwargs: Any) -> None:\n        self.responses.append([command, code, text, kwargs])\n        self.status_code = code\n        self.status_text = text\n        self.last_command = command\n\n    @property\n    def success(self) -> bool:\n        return self._finished and self.status_code is not None and self.status_code == 250\n\n    def __repr__(self) -> str:\n        return \"<emails.backend.SMTPResponse status_code=%s status_text=%s>\" % (self.status_code.__repr__(),\n                                                                                self.status_text.__repr__())\n\n"
  },
  {
    "path": "emails/backend/smtp/__init__.py",
    "content": "\nfrom .backend import SMTPBackend\n\ntry:\n    from .aio_backend import AsyncSMTPBackend\nexcept ImportError:\n    pass"
  },
  {
    "path": "emails/backend/smtp/aio_backend.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import Any\n\nimport aiosmtplib\n\nfrom ..response import SMTPResponse\nfrom .aio_client import AsyncSMTPClientWithResponse\nfrom ...utils import DNS_NAME\nfrom .exceptions import SMTPConnectNetworkError\n\n\n__all__ = ['AsyncSMTPBackend']\n\nlogger = logging.getLogger(__name__)\n\n\nclass AsyncSMTPBackend:\n\n    \"\"\"\n    AsyncSMTPBackend manages an async SMTP connection using aiosmtplib.\n    \"\"\"\n\n    DEFAULT_SOCKET_TIMEOUT = 5\n\n    response_cls = SMTPResponse\n\n    def __init__(self, ssl: bool = False, fail_silently: bool = True,\n                 mail_options: list[str] | None = None, **kwargs: Any) -> None:\n\n        self.ssl = ssl\n        self.tls = kwargs.get('tls')\n        if self.ssl and self.tls:\n            raise ValueError(\n                \"ssl/tls are mutually exclusive, so only set \"\n                \"one of those settings to True.\")\n\n        kwargs.setdefault('timeout', self.DEFAULT_SOCKET_TIMEOUT)\n        kwargs.setdefault('local_hostname', DNS_NAME.get_fqdn())\n        kwargs['port'] = int(kwargs.get('port', 0))\n\n        self.smtp_cls_kwargs = kwargs\n\n        self.host: str | None = kwargs.get('host')\n        self.port: int = kwargs['port']\n        self.fail_silently = fail_silently\n        self.mail_options = mail_options or []\n\n        self._client: AsyncSMTPClientWithResponse | None = None\n        self._lock = asyncio.Lock()\n\n    async def get_client(self) -> AsyncSMTPClientWithResponse:\n        async with self._lock:\n            return await self._get_client_unlocked()\n\n    async def _get_client_unlocked(self) -> AsyncSMTPClientWithResponse:\n        if self._client is None:\n            client = AsyncSMTPClientWithResponse(\n                parent=self, ssl=self.ssl, **self.smtp_cls_kwargs\n            )\n            await client.initialize()\n            self._client = client\n        return self._client\n\n    async def close(self) -> None:\n        \"\"\"Closes the connection to the email server.\"\"\"\n        async with self._lock:\n            await self._close_unlocked()\n\n    async def _close_unlocked(self) -> None:\n        if self._client:\n            try:\n                await self._client.quit()\n            except Exception:\n                if self.fail_silently:\n                    return\n                raise\n            finally:\n                self._client = None\n\n    def make_response(self, exception: Exception | None = None) -> SMTPResponse:\n        return self.response_cls(backend=self, exception=exception)\n\n    async def _send(self, **kwargs: Any) -> SMTPResponse | None:\n        response = None\n        try:\n            client = await self._get_client_unlocked()\n        except aiosmtplib.SMTPConnectError as exc:\n            cause = exc.__cause__\n            if isinstance(cause, IOError):\n                response = self.make_response(\n                    exception=SMTPConnectNetworkError.from_ioerror(cause))\n            else:\n                response = self.make_response(exception=exc)\n            if not self.fail_silently:\n                raise\n        except aiosmtplib.SMTPException as exc:\n            response = self.make_response(exception=exc)\n            if not self.fail_silently:\n                raise\n        except IOError as exc:\n            response = self.make_response(\n                exception=SMTPConnectNetworkError.from_ioerror(exc))\n            if not self.fail_silently:\n                raise\n\n        if response:\n            return response\n        else:\n            return await client.sendmail(**kwargs)\n\n    async def _send_with_retry(self, **kwargs: Any) -> SMTPResponse | None:\n        async with self._lock:\n            try:\n                return await self._send(**kwargs)\n            except aiosmtplib.SMTPServerDisconnected:\n                logger.debug('SMTPServerDisconnected, retry once')\n                await self._close_unlocked()\n                return await self._send(**kwargs)\n\n    async def sendmail(self, from_addr: str, to_addrs: str | list[str],\n                       msg: Any, mail_options: list[str] | None = None,\n                       rcpt_options: list[str] | None = None) -> SMTPResponse | None:\n\n        if not to_addrs:\n            return None\n\n        if not isinstance(to_addrs, (list, tuple)):\n            to_addrs = [to_addrs]\n\n        response = await self._send_with_retry(\n            from_addr=from_addr,\n            to_addrs=to_addrs,\n            msg=msg.as_bytes(),\n            mail_options=mail_options or self.mail_options,\n            rcpt_options=rcpt_options,\n        )\n\n        if response and not self.fail_silently:\n            response.raise_if_needed()\n\n        return response\n\n    async def __aenter__(self) -> AsyncSMTPBackend:\n        return self\n\n    async def __aexit__(self, exc_type: type[BaseException] | None,\n                        exc_value: BaseException | None,\n                        traceback: Any | None) -> None:\n        await self.close()\n"
  },
  {
    "path": "emails/backend/smtp/aio_client.py",
    "content": "from __future__ import annotations\n\n__all__ = [\"AsyncSMTPClientWithResponse\"]\n\nimport logging\nfrom typing import TYPE_CHECKING\n\nimport aiosmtplib\n\nfrom ..response import SMTPResponse\nfrom ...utils import sanitize_email\n\nif TYPE_CHECKING:\n    from .aio_backend import AsyncSMTPBackend\n\nlogger = logging.getLogger(__name__)\n\n\nclass AsyncSMTPClientWithResponse:\n    \"\"\"Async SMTP client built on aiosmtplib that returns SMTPResponse objects.\"\"\"\n\n    def __init__(self, parent: AsyncSMTPBackend, **kwargs):\n        self.parent = parent\n        self.make_response = parent.make_response\n\n        self.tls = kwargs.pop(\"tls\", False)\n        self.ssl = kwargs.pop(\"ssl\", False)\n        self.debug = kwargs.pop(\"debug\", 0)\n        if self.debug:\n            logger.warning(\n                \"debug parameter is not supported in async mode; \"\n                \"use Python logging instead\"\n            )\n        self.user = kwargs.pop(\"user\", None)\n        self.password = kwargs.pop(\"password\", None)\n\n        # aiosmtplib uses use_tls for implicit TLS (SMTPS) and\n        # start_tls for STARTTLS after connect\n        smtp_kwargs = dict(kwargs)\n        smtp_kwargs[\"use_tls\"] = self.ssl\n        smtp_kwargs[\"start_tls\"] = self.tls\n\n        # aiosmtplib uses 'hostname' instead of 'host'\n        if \"host\" in smtp_kwargs:\n            smtp_kwargs[\"hostname\"] = smtp_kwargs.pop(\"host\")\n\n        self._smtp = aiosmtplib.SMTP(**smtp_kwargs)\n        self._esmtp = False\n\n    async def initialize(self):\n        await self._smtp.connect()\n        # connect() may have already completed EHLO internally\n        self._esmtp = self._smtp.supports_esmtp\n        try:\n            if self._smtp.is_ehlo_or_helo_needed:\n                try:\n                    await self._smtp.ehlo()\n                    self._esmtp = True\n                except aiosmtplib.SMTPHeloError:\n                    # aiosmtplib closes the transport on 421 responses\n                    # before raising; don't attempt HELO on a dead connection\n                    if not self._smtp.is_connected:\n                        raise\n                    await self._smtp.helo()\n                    # aiosmtplib sets supports_esmtp before checking the\n                    # response code, so it may be True even after EHLO\n                    # failed.  Track the real state ourselves.\n                    self._esmtp = False\n            if self.user:\n                await self._smtp.login(self.user, self.password)\n        except Exception:\n            await self.quit()\n            raise\n\n    async def quit(self):\n        \"\"\"Closes the connection to the email server.\"\"\"\n        try:\n            await self._smtp.quit()\n        except (aiosmtplib.SMTPServerDisconnected, ConnectionError):\n            self._smtp.close()\n\n    async def _rset(self):\n        try:\n            await self._smtp.rset()\n        except (aiosmtplib.SMTPServerDisconnected, ConnectionError):\n            pass\n\n    async def sendmail(\n        self,\n        from_addr: str,\n        to_addrs: list[str] | str,\n        msg: bytes,\n        mail_options: list[str] | None = None,\n        rcpt_options: list[str] | None = None,\n    ) -> SMTPResponse | None:\n\n        if not to_addrs:\n            return None\n\n        rcpt_options = rcpt_options or []\n        mail_options = mail_options or []\n        esmtp_opts = []\n        if self._esmtp:\n            if self._smtp.supports_extension(\"size\"):\n                esmtp_opts.append(\"size=%d\" % len(msg))\n            for option in mail_options:\n                esmtp_opts.append(option)\n\n        response = self.make_response()\n\n        from_addr = sanitize_email(from_addr)\n\n        response.from_addr = from_addr\n        response.esmtp_opts = esmtp_opts[:]\n\n        try:\n            resp = await self._smtp.mail(from_addr, options=esmtp_opts)\n        except aiosmtplib.SMTPSenderRefused as exc:\n            response.set_status(\n                \"mail\",\n                exc.code,\n                exc.message.encode() if isinstance(exc.message, str) else exc.message,\n            )\n            response.set_exception(exc)\n            await self._rset()\n            return response\n\n        response.set_status(\n            \"mail\",\n            resp.code,\n            resp.message.encode() if isinstance(resp.message, str) else resp.message,\n        )\n\n        if resp.code != 250:\n            await self._rset()\n            response.set_exception(\n                aiosmtplib.SMTPSenderRefused(resp.code, resp.message, from_addr)\n            )\n            return response\n\n        if not isinstance(to_addrs, (list, tuple)):\n            to_addrs = [to_addrs]\n\n        to_addrs = [sanitize_email(e) for e in to_addrs]\n\n        response.to_addrs = to_addrs\n        response.rcpt_options = rcpt_options[:]\n        response.refused_recipients = {}\n\n        for a in to_addrs:\n            try:\n                resp = await self._smtp.rcpt(a, options=rcpt_options)\n                code = resp.code\n                resp_msg = (\n                    resp.message.encode()\n                    if isinstance(resp.message, str)\n                    else resp.message\n                )\n            except aiosmtplib.SMTPRecipientRefused as exc:\n                code = exc.code\n                resp_msg = (\n                    exc.message.encode()\n                    if isinstance(exc.message, str)\n                    else exc.message\n                )\n\n            response.set_status(\"rcpt\", code, resp_msg, recipient=a)\n            if (code != 250) and (code != 251):\n                response.refused_recipients[a] = (code, resp_msg)\n\n        if len(response.refused_recipients) == len(to_addrs):\n            await self._rset()\n            refused_list = [\n                aiosmtplib.SMTPRecipientRefused(\n                    code, msg.decode() if isinstance(msg, bytes) else msg, addr\n                )\n                for addr, (code, msg) in response.refused_recipients.items()\n            ]\n            response.set_exception(aiosmtplib.SMTPRecipientsRefused(refused_list))\n            return response\n\n        try:\n            resp = await self._smtp.data(msg)\n        except aiosmtplib.SMTPDataError as exc:\n            resp_msg = (\n                exc.message.encode() if isinstance(exc.message, str) else exc.message\n            )\n            response.set_status(\"data\", exc.code, resp_msg)\n            response.set_exception(exc)\n            await self._rset()\n            return response\n\n        resp_msg = (\n            resp.message.encode() if isinstance(resp.message, str) else resp.message\n        )\n        response.set_status(\"data\", resp.code, resp_msg)\n        if resp.code != 250:\n            await self._rset()\n            response.set_exception(aiosmtplib.SMTPDataError(resp.code, resp.message))\n            return response\n\n        response._finished = True\n        return response\n"
  },
  {
    "path": "emails/backend/smtp/backend.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport smtplib\nfrom collections.abc import Callable\nfrom functools import wraps\nfrom types import TracebackType\nfrom typing import Any\n\nfrom ..response import SMTPResponse\nfrom .client import SMTPClientWithResponse, SMTPClientWithResponse_SSL\nfrom ...utils import DNS_NAME\nfrom .exceptions import SMTPConnectNetworkError\n\n\n__all__ = ['SMTPBackend']\n\nlogger = logging.getLogger(__name__)\n\n\nclass SMTPBackend:\n\n    \"\"\"\n    SMTPBackend manages a smtp connection.\n    \"\"\"\n\n    DEFAULT_SOCKET_TIMEOUT = 5\n\n    connection_cls = SMTPClientWithResponse\n    connection_ssl_cls = SMTPClientWithResponse_SSL\n    response_cls = SMTPResponse\n\n    def __init__(self, ssl: bool = False, fail_silently: bool = True,\n                 mail_options: list[str] | None = None, **kwargs: Any) -> None:\n\n        self.smtp_cls = self.connection_ssl_cls if ssl else self.connection_cls\n\n        self.ssl = ssl\n        self.tls = kwargs.get('tls')\n        if self.ssl and self.tls:\n            raise ValueError(\n                \"ssl/tls are mutually exclusive, so only set \"\n                \"one of those settings to True.\")\n\n        kwargs.setdefault('timeout', self.DEFAULT_SOCKET_TIMEOUT)\n        kwargs.setdefault('local_hostname', DNS_NAME.get_fqdn())\n        kwargs['port'] = int(kwargs.get('port', 0))  # Issue #85\n\n        self.smtp_cls_kwargs = kwargs\n\n        self.host: str | None = kwargs.get('host')\n        self.port: int = kwargs['port']  # always set as int two lines above\n        self.fail_silently = fail_silently\n        self.mail_options = mail_options or []\n\n        self._client: SMTPClientWithResponse | None = None\n\n    def get_client(self) -> SMTPClientWithResponse:\n        if self._client is None:\n            self._client = self.smtp_cls(parent=self, **self.smtp_cls_kwargs)\n        return self._client\n\n    def close(self) -> None:\n\n        \"\"\"\n        Closes the connection to the email server.\n        \"\"\"\n\n        if self._client:\n            try:\n                self._client.quit()\n            except Exception:\n                    if self.fail_silently:\n                        return\n                    raise\n            finally:\n                self._client = None\n\n    def make_response(self, exception: Exception | None = None) -> SMTPResponse:\n        return self.response_cls(backend=self, exception=exception)\n\n    def retry_on_disconnect(self, func: Callable[..., SMTPResponse | None]) -> Callable[..., SMTPResponse | None]:\n        @wraps(func)\n        def wrapper(*args: Any, **kwargs: Any) -> SMTPResponse | None:\n            try:\n                return func(*args, **kwargs)\n            except smtplib.SMTPServerDisconnected:\n                # If server disconected, clear old client\n                logger.debug('SMTPServerDisconnected, retry once')\n                self.close()\n                return func(*args, **kwargs)\n        return wrapper\n\n    def _send(self, **kwargs: Any) -> SMTPResponse | None:\n\n        response = None\n        try:\n            client = self.get_client()\n        except smtplib.SMTPException as exc:\n            response = self.make_response(exception=exc)\n            if not self.fail_silently:\n                raise\n        except IOError as exc:\n            response = self.make_response(exception=SMTPConnectNetworkError.from_ioerror(exc))\n            if not self.fail_silently:\n                raise\n\n        if response:\n            return response\n        else:\n            return client.sendmail(**kwargs)\n\n    def sendmail(self, from_addr: str, to_addrs: str | list[str],\n                 msg: Any, mail_options: list[str] | None = None,\n                 rcpt_options: list[str] | None = None) -> SMTPResponse | None:\n\n        if not to_addrs:\n            return None\n\n        if not isinstance(to_addrs, (list, tuple)):\n            to_addrs = [to_addrs, ]\n\n        send = self.retry_on_disconnect(self._send)\n\n        response = send(from_addr=from_addr,\n                        to_addrs=to_addrs,\n                        msg=msg.as_bytes(),\n                        mail_options=mail_options or self.mail_options,\n                        rcpt_options=rcpt_options)\n\n        if response and not self.fail_silently:\n            response.raise_if_needed()\n\n        return response\n\n    def __enter__(self) -> SMTPBackend:\n        return self\n\n    def __exit__(self, exc_type: type[BaseException] | None,\n                 exc_value: BaseException | None,\n                 traceback: TracebackType | None) -> None:\n        self.close()\n"
  },
  {
    "path": "emails/backend/smtp/client.py",
    "content": "from __future__ import annotations\n\n__all__ = ['SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']\n\nimport smtplib\nfrom smtplib import _have_ssl, SMTP  # noqa: private API\nimport logging\nfrom ..response import SMTPResponse\nfrom ...utils import sanitize_email\n\nlogger = logging.getLogger(__name__)\n\n\nclass SMTPClientWithResponse(SMTP):\n\n    def __init__(self, parent, **kwargs):\n\n        self._initialized = False\n\n        self.parent = parent\n        self.make_response = parent.make_response\n        self.tls = kwargs.pop('tls', False)\n        self.ssl = kwargs.pop('ssl', False)\n        self.debug = kwargs.pop('debug', 0)\n        self.user = kwargs.pop('user', None)\n        self.password = kwargs.pop('password', None)\n\n        SMTP.__init__(self, **kwargs)\n\n        try:\n            self.initialize()\n        except Exception:\n            self.quit()\n            raise\n\n\n    def initialize(self):\n        if not self._initialized:\n            self.set_debuglevel(self.debug)\n            if self.tls:\n                self.starttls()\n            if self.user:\n                self.login(user=self.user, password=self.password)\n            self.ehlo_or_helo_if_needed()\n            self.initialized = True\n\n    def quit(self):\n        \"\"\"Closes the connection to the email server.\"\"\"\n        try:\n            SMTP.quit(self)\n        except (smtplib.SMTPServerDisconnected, ):\n            self.close()\n\n    def _rset(self):\n        try:\n            self.rset()\n        except smtplib.SMTPServerDisconnected:\n            pass\n\n    def sendmail(self, from_addr: str, to_addrs: list[str] | str,\n                 msg: bytes, mail_options: list[str] | None = None,\n                 rcpt_options: list[str] | None = None) -> SMTPResponse | None:\n\n        if not to_addrs:\n            return None\n\n        rcpt_options = rcpt_options or []\n        mail_options = mail_options or []\n        esmtp_opts = []\n        if self.does_esmtp:\n            if self.has_extn('size'):\n                esmtp_opts.append(\"size=%d\" % len(msg))\n            for option in mail_options:\n                esmtp_opts.append(option)\n\n        response = self.make_response()\n\n        from_addr = sanitize_email(from_addr)\n\n        response.from_addr = from_addr\n        response.esmtp_opts = esmtp_opts[:]\n\n        (code, resp) = self.mail(from_addr, esmtp_opts)\n        response.set_status('mail', code, resp)\n\n        if code != 250:\n            self._rset()\n            exc = smtplib.SMTPSenderRefused(code, resp, from_addr)\n            response.set_exception(exc)\n            return response\n\n        if not isinstance(to_addrs, (list, tuple)):\n            to_addrs = [to_addrs]\n\n        to_addrs = [sanitize_email(e) for e in to_addrs]\n\n        response.to_addrs = to_addrs\n        response.rcpt_options = rcpt_options[:]\n        response.refused_recipients = {}\n\n        for a in to_addrs:\n            (code, resp) = self.rcpt(a, rcpt_options)\n            response.set_status('rcpt', code, resp, recipient=a)\n            if (code != 250) and (code != 251):\n                response.refused_recipients[a] = (code, resp)\n\n        if len(response.refused_recipients) == len(to_addrs):\n            # the server refused all our recipients\n            self._rset()\n            exc = smtplib.SMTPRecipientsRefused(response.refused_recipients)\n            response.set_exception(exc)\n            return response\n\n        (code, resp) = self.data(msg)\n        response.set_status('data', code, resp)\n        if code != 250:\n            self._rset()\n            exc = smtplib.SMTPDataError(code, resp)\n            response.set_exception(exc)\n            return response\n\n        response._finished = True\n        return response\n\n\nif _have_ssl:\n\n    from smtplib import SMTP_SSL\n    import ssl\n\n    class SMTPClientWithResponse_SSL(SMTP_SSL, SMTPClientWithResponse):\n\n        def __init__(self, **kw):\n            args = {}\n            for k in ('host', 'port', 'local_hostname', 'keyfile', 'certfile', 'timeout'):\n                if k in kw:\n                    args[k] = kw[k]\n            SMTP_SSL.__init__(self, **args)\n            SMTPClientWithResponse.__init__(self, **kw)\n\n        def _rset(self):\n            try:\n                self.rset()\n            except (ssl.SSLError, smtplib.SMTPServerDisconnected):\n                pass\n\n        def quit(self):\n            \"\"\"Closes the connection to the email server.\"\"\"\n            try:\n                SMTPClientWithResponse.quit(self)\n            except (ssl.SSLError, smtplib.SMTPServerDisconnected):\n                # This happens when calling quit() on a TLS connection\n                # sometimes, or when the connection was already disconnected\n                # by the server.\n                self.close()\n\n        def sendmail(self, *args, **kw):\n            return SMTPClientWithResponse.sendmail(self, *args, **kw)\n\nelse:\n\n    class SMTPClientWithResponse_SSL:\n        def __init__(self, *args, **kwargs):\n            # should raise import error here\n            import ssl\n\n\n\n"
  },
  {
    "path": "emails/backend/smtp/exceptions.py",
    "content": "import socket\n\n\nclass SMTPConnectNetworkError(IOError):\n    \"\"\"Network error during connection establishment.\"\"\"\n\n    @classmethod\n    def from_ioerror(cls, exc):\n        o = cls()\n        o.errno = exc.errno\n        o.filename = exc.filename\n        o.strerror = exc.strerror or str(exc)\n        return o\n"
  },
  {
    "path": "emails/django/__init__.py",
    "content": "from django.core.mail import get_connection\nfrom .. message import MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage\nfrom .. utils import sanitize_email\n\n__all__ = ['DjangoMessageMixin', 'DjangoMessage']\n\n\nclass DjangoMessageMixin(object):\n\n    _recipients = None\n    _from_email = None\n\n    @property\n    def encoding(self):\n        return self.charset or 'utf-8'\n\n    def recipients(self):\n        ret = self._recipients\n        if ret is None:\n            ret = self.get_recipients_emails()\n        return [sanitize_email(e) for e in ret]\n\n    @property\n    def from_email(self):\n        return sanitize_email(self._from_email or self.mail_from[1])\n\n    def _set_emails(self, mail_to=None, set_mail_to=True, mail_from=None,\n                    set_mail_from=False, to=None):\n\n        self._recipients = None\n        self._from_email = None\n\n        mail_to = mail_to or to  # \"to\" is legacy\n\n        if mail_to is not None:\n            if set_mail_to:\n                self.mail_to = mail_to\n            else:\n                self._recipients = [mail_to, ]\n\n        if mail_from is not None:\n            if set_mail_from:\n                self.mail_from = mail_from\n            else:\n                self._from_email = mail_from\n\n    def send(self, mail_to=None, set_mail_to=True, mail_from=None, set_mail_from=False,\n             context=None, connection=None, to=None):\n\n        self._set_emails(mail_to=mail_to, set_mail_to=set_mail_to,\n                         mail_from=mail_from, set_mail_from=set_mail_from, to=to)\n\n        if context is not None:\n            self.render(**context)\n\n        connection = connection or get_connection()\n        return connection.send_messages([self, ])\n\n\nclass DjangoMessage(DjangoMessageMixin, MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage):\n    \"\"\"\n    Send via django email smtp backend\n    \"\"\"\n    pass\n\nMessage = DjangoMessage\n"
  },
  {
    "path": "emails/django_.py",
    "content": "import warnings\nwarnings.warn(\"emails.django_ module moved to emails.django\", DeprecationWarning)\n\nfrom .django import *"
  },
  {
    "path": "emails/exc.py",
    "content": "from __future__ import annotations\n\nfrom dkim import DKIMException\n\n\nclass HTTPLoaderError(Exception):\n    pass\n\n\nclass BadHeaderError(ValueError):\n    pass\n\n\nclass IncompleteMessage(ValueError):\n    pass"
  },
  {
    "path": "emails/loader/__init__.py",
    "content": "import os.path\nfrom email.utils import formataddr\n\nimport urllib.parse as urlparse\n\nfrom ..message import Message\nfrom ..utils import fetch_url\nfrom .local_store import (FileSystemLoader, ZipLoader, MsgLoader, FileNotFound)\nfrom .helpers import guess_charset\n\nclass LoadError(Exception):\n    pass\n\n\nclass IndexFileNotFound(LoadError):\n    pass\n\n\nclass InvalidHtmlFile(LoadError):\n    pass\n\n\ndef from_html(html, text=None, base_url=None, message_params=None, local_loader=None,\n              template_cls=None, message_cls=None, source_filename=None, requests_params=None,\n              **kwargs):\n\n    \"\"\"\n    Loads message from html string with images from local_loader.\n\n    :param html: html string\n    :param base_url: base_url for html\n    :param text: text string or None\n    :param template_cls: if set, html and text are set with this template class\n    :param local_loader: loader with local files\n    :param message_cls: default is emails.Message\n    :param message_params: parameters for Message constructor\n    :param source_filename: source html file name (used for exception description on html parsing error)\n    :param requests_params: parameters for external url handling\n    :param kwargs: arguments for transformer.load_and_transform\n    :return:\n    \"\"\"\n\n    if template_cls is None:\n        template_cls = lambda x: x\n\n    message_params = message_params or {}\n\n    _param_html = message_params.pop('html', None)\n    _param_text = message_params.pop('text', None)\n\n    message = (message_cls or Message)(html=template_cls(html or _param_html or ''),\n                                       text=template_cls(text or _param_text),\n                                       **message_params)\n    message.create_transformer(requests_params=requests_params,\n                               base_url=base_url,\n                               local_loader=local_loader)\n    if message.transformer.tree is None:\n        raise InvalidHtmlFile(\"Error parsing '%s'\" % source_filename)\n    message.transformer.load_and_transform(**kwargs)\n    message.transformer.save()\n    message._loader = local_loader\n    return message\n\n\nfrom_string = from_html\n\n\ndef from_url(url, requests_params=None, **kwargs):\n\n    def _extract_base_url(url):\n        # /a/b.html -> /a\n        p = list(urlparse.urlparse(url))[:5]\n        p[2] = os.path.split(p[2])[0]\n        return urlparse.urlunsplit(p)\n\n    # Load html page\n    r = fetch_url(url, requests_args=requests_params)\n    html = r.content\n    html = html.decode(guess_charset(r.headers, html) or 'utf-8')\n    html = html.replace('\\r\\n', '\\n')  # Remove \\r\n\n    return from_html(html,\n                     base_url=_extract_base_url(url),\n                     source_filename=url,\n                     requests_params=requests_params,\n                     **kwargs)\n\n\nload_url = from_url\n\n\ndef _from_filebased_source(store, skip_html=False, html_filename=None, skip_text=True, text_filename=None,\n                           message_params=None, **kwargs):\n    \"\"\"\n    Loads message from prepared store `store`.\n\n    :param store: prepared filestore\n    :param skip_html: if True, make message without html part\n    :param html_filename: html part filename. If None, search automatically.\n    :param skip_text: if True, make message without text part\n    :param text_filename: text part filename. If None, search automatically.\n    :param message_params: parameters for Message\n    :param kwargs: arguments for from_html\n    :return:\n    \"\"\"\n\n    if not skip_html:\n        try:\n            html_filename = store.find_index_html(html_filename)\n        except FileNotFound:\n            raise IndexFileNotFound('html file not found')\n\n    dirname, html_filename = os.path.split(html_filename)\n    if dirname:\n        store.base_path = dirname\n\n    html = store.content(html_filename, is_html=True, guess_charset=True)\n\n    text = None\n    if not skip_text:\n        text_filename = store.find_index_text(text_filename)\n        text = text_filename and store.content(text_filename) or None\n\n    return from_html(html=html,\n                     text=text,\n                     local_loader=store,\n                     source_filename=html_filename,\n                     message_params=message_params,\n                     **kwargs)\n\n\ndef from_directory(directory, loader_cls=None, **kwargs):\n    \"\"\"\n    Loads message from local directory.\n    Can guess for html and text part filenames (if parameters set).\n\n    :param directory: directory path\n    :param kwargs: arguments for _from_filebased_source function\n    :return: emails.Message object\n    \"\"\"\n\n    loader_cls = loader_cls or FileSystemLoader\n    return _from_filebased_source(store=loader_cls(searchpath=directory), **kwargs)\n\n\ndef from_file(filename, **kwargs):\n    \"\"\"\n    Loads message from local file.\n    File `filename` must be html file.\n\n    :param filename: filename\n    :param kwargs: arguments for _from_filebased_source function\n    :return: emails.Message object\n    \"\"\"\n    return from_directory(directory=os.path.dirname(filename), html_filename=os.path.basename(filename), **kwargs)\n\n\ndef from_zip(zip_file, loader_cls=None, **kwargs):\n    \"\"\"\n    Loads message from zipfile.\n\n    :param zip_file: file-like object with zip file\n    :param kwargs: arguments for _from_filebased_source function\n    :return: emails.Message object\n    \"\"\"\n    loader_cls = loader_cls or ZipLoader\n    return _from_filebased_source(store=loader_cls(file=zip_file), **kwargs)\n\n\ndef from_rfc822(msg, loader_cls=None, message_params=None, parse_headers=False):\n    # Warning: from_rfc822 is for demo purposes only\n    message_params = message_params or {}\n    loader_cls = loader_cls or MsgLoader\n\n    loader = loader_cls(msg=msg)\n    message = Message(html=loader.html, text=loader.text, **message_params)\n    message._loader = loader\n\n    for att in loader.attachments:\n        message.attachments.add(att)\n\n    if parse_headers:\n        loader.copy_headers_to_message(message)\n\n    return message"
  },
  {
    "path": "emails/loader/helpers.py",
    "content": "__all__ = ['guess_charset', 'fix_content_type']\nfrom email.message import Message\n\n\nimport re\nimport warnings\n\ntry:\n    import charade as chardet\n    warnings.warn(\"charade module is deprecated, update your requirements to chardet\",\n                  DeprecationWarning)\nexcept ImportError:\n    import chardet\n\n\n# HTML page charset stuff\n\nclass ReRules:\n    re_meta = b\"(?i)(?<=<meta).*?(?=>)\"\n    re_is_http_equiv = b\"http-equiv=\\\"?'?content-type\\\"?'?\"\n    re_parse_http_equiv = b\"content=\\\"?'?([^\\\"'>]+)\"\n    re_charset = b\"charset=\\\"?'?([\\\\w-]+)\\\"?'?\"\n\n    def __init__(self, conv=None):\n        if conv is None:\n            conv = lambda x: x\n        for k in dir(self):\n            if k.startswith('re_'):\n                setattr(self, k, re.compile(conv(getattr(self, k)), re.I + re.S + re.M))\n\nRULES_U = ReRules(conv=lambda x: x.decode())\nRULES_B = ReRules()\n\n\ndef guess_text_charset(text, is_html=False):\n    if is_html:\n        is_bytes = isinstance(text, bytes)\n        rules = RULES_B if is_bytes else RULES_U\n        for meta in rules.re_meta.findall(text):\n            if rules.re_is_http_equiv.findall(meta):\n                for content in rules.re_parse_http_equiv.findall(meta):\n                    for charset in rules.re_charset.findall(content):\n                        return charset.decode() if is_bytes else charset\n            else:\n                for charset in rules.re_charset.findall(meta):\n                    return charset.decode() if is_bytes else charset\n    # guess by chardet\n    if isinstance(text, bytes):\n        return chardet.detect(text)['encoding']\n\n\ndef guess_html_charset(html):\n    return guess_text_charset(text=html, is_html=True)\n\n\ndef guess_charset(headers, html):\n\n    # guess by http headers\n    if headers:\n        content_type = headers['content-type']\n        if content_type:\n            msg = Message()\n            msg.add_header('content-type', content_type)\n            r = msg.get_param('charset')\n            if r:\n                return r\n\n    # guess by html content\n    charset = guess_html_charset(html)\n    if charset:\n        return charset\n\nCOMMON_CHARSETS = ('ascii', 'utf-8', 'utf-16', 'windows-1251', 'windows-1252', 'cp850')\n\ndef decode_text(text,\n                is_html=False,\n                guess_charset=True,\n                try_common_charsets=True,\n                charsets=None,\n                fallback_charset='utf-8'):\n\n    if not isinstance(text, bytes):\n        return text, None\n\n    _charsets = []\n    if guess_charset:\n        c = guess_text_charset(text, is_html=is_html)\n        if c:\n            _charsets.append(c)\n\n    if charsets:\n        _charsets.extend(charsets)\n\n    if try_common_charsets:\n        _charsets.extend(COMMON_CHARSETS)\n\n    if fallback_charset:\n        _charsets.append(fallback_charset)\n\n    _last_exc = None\n    for enc in _charsets:\n        try:\n            return text.decode(enc), enc\n        except UnicodeDecodeError as exc:\n            _last_exc = exc\n\n    raise _last_exc\n"
  },
  {
    "path": "emails/message.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom datetime import datetime\nfrom email.utils import getaddresses\nfrom typing import Any, IO\n\nfrom .utils import (formataddr,\n                    SafeMIMEText, SafeMIMEMultipart, sanitize_address,\n                    parse_name_and_email, load_email_charsets,\n                    encode_header as encode_header_,\n                    renderable, format_date_header, parse_name_and_email_list,\n                    cached_property, MessageID)\nfrom .exc import BadHeaderError\nfrom .backend import ObjectFactory, SMTPBackend\nfrom .store import MemoryFileStore, BaseFile\nfrom .signers import DKIMSigner\n\n\nload_email_charsets()  # sic!\n\n\n# Type alias for email addresses accepted by the public API\n_Address = str | tuple[str | None, str] | None\n_AddressList = str | tuple[str | None, str] | list[str | tuple[str | None, str]] | None\n\n\nclass BaseMessage:\n\n    \"\"\"\n    Base email message with html part, text part and attachments.\n    \"\"\"\n\n    attachment_cls = BaseFile\n    filestore_cls = MemoryFileStore\n    policy = None\n\n    def __init__(self,\n                 charset: str | None = None,\n                 message_id: str | MessageID | bool | None = None,\n                 date: str | datetime | float | bool | Callable[..., str | datetime | float] | None = None,\n                 subject: str | None = None,\n                 mail_from: _Address = None,\n                 mail_to: _AddressList = None,\n                 headers: dict[str, str] | None = None,\n                 html: str | IO[str] | None = None,\n                 text: str | IO[str] | None = None,\n                 attachments: list[dict[str, Any] | BaseFile] | None = None,\n                 cc: _AddressList = None,\n                 bcc: _AddressList = None,\n                 headers_encoding: str | None = None,\n                 reply_to: _AddressList = None) -> None:\n\n        self._attachments: MemoryFileStore | None = None\n        self.charset: str = charset or 'utf-8'\n        self.headers_encoding: str = headers_encoding or 'ascii'\n        self._message_id = message_id\n        self.set_subject(subject)\n        self.set_date(date)\n        self.set_mail_from(mail_from)\n        self.set_mail_to(mail_to)\n        self.set_cc(cc)\n        self.set_bcc(bcc)\n        self.set_reply_to(reply_to)\n        self.set_headers(headers)\n        self.set_html(html=html)\n        self.set_text(text=text)\n        self.render_data: dict[str, Any] = {}\n\n        if attachments:\n            for a in attachments:\n                self.attachments.add(a)\n\n    def set_mail_from(self, mail_from: _Address) -> None:\n        # In: ('Alice', '<alice@me.com>' )\n        self._mail_from = mail_from and parse_name_and_email(mail_from) or None\n\n    def get_mail_from(self) -> tuple[str | None, str | None] | None:\n        # Out: ('Alice', '<alice@me.com>') or None\n        return self._mail_from\n\n    mail_from = property(get_mail_from, set_mail_from)\n\n    def set_mail_to(self, mail_to: _AddressList) -> None:\n        self._mail_to = parse_name_and_email_list(mail_to)\n\n    def get_mail_to(self) -> list[tuple[str | None, str | None]]:\n        return self._mail_to\n\n    mail_to = property(get_mail_to, set_mail_to)\n\n    def set_cc(self, addr: _AddressList) -> None:\n        self._cc = parse_name_and_email_list(addr)\n\n    def get_cc(self) -> list[tuple[str | None, str | None]]:\n        return self._cc\n\n    cc = property(get_cc, set_cc)\n\n    def set_bcc(self, addr: _AddressList) -> None:\n        self._bcc = parse_name_and_email_list(addr)\n\n    def get_bcc(self) -> list[tuple[str | None, str | None]]:\n        return self._bcc\n\n    bcc = property(get_bcc, set_bcc)\n\n    def set_reply_to(self, addr: _AddressList) -> None:\n        self._reply_to = parse_name_and_email_list(addr)\n\n    def get_reply_to(self) -> list[tuple[str | None, str | None]]:\n        return self._reply_to\n\n    reply_to = property(get_reply_to, set_reply_to)\n\n    def get_recipients_emails(self) -> list[str | None]:\n        \"\"\"\n        Returns message recipient's emails for actual sending.\n        :return: list of emails\n        \"\"\"\n        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]))\n\n    def set_headers(self, headers: dict[str, str] | None) -> None:\n        self._headers = headers or {}\n\n    def set_html(self, html: str | IO[str] | None, url: str | None = None) -> None:\n        if hasattr(html, 'read'):\n            html = html.read()\n        self._html = html\n        self._html_url = url\n\n    def get_html(self) -> str | None:\n        return self._html\n\n    html = property(get_html, set_html)\n\n    def set_text(self, text: str | IO[str] | None, url: str | None = None) -> None:\n        if hasattr(text, 'read'):\n            text = text.read()\n        self._text = text\n        self._text_url = url\n\n    def get_text(self) -> str | None:\n        return self._text\n\n    text = property(get_text, set_text)\n\n    @property\n    @renderable\n    def html_body(self) -> str | None:\n        return self._html\n\n    @property\n    @renderable\n    def text_body(self) -> str | None:\n        return self._text\n\n    def set_subject(self, value: str | None) -> None:\n        self._subject = value\n\n    @renderable\n    def get_subject(self) -> str | None:\n        return self._subject\n\n    subject = property(get_subject, set_subject)\n\n    def render(self, **kwargs: Any) -> None:\n        self.render_data = kwargs\n\n    def set_date(self, value: str | datetime | float | bool | Callable[..., str | datetime | float] | None) -> None:\n        self._date = value\n\n    def get_date(self) -> str | None:\n        v = self._date\n        if v is False:\n            return None\n        if callable(v):\n            v = v()\n        if not isinstance(v, str):\n            v = format_date_header(v)\n        return v\n\n    date = property(get_date, set_date)\n    message_date = date\n\n    @property\n    def message_id(self) -> str | None:\n        mid = self._message_id\n        if mid is False:\n            return None\n        return callable(mid) and mid() or mid\n\n    @message_id.setter\n    def message_id(self, value: str | MessageID | bool | None) -> None:\n        self._message_id = value\n\n    @property\n    def attachments(self) -> MemoryFileStore:\n        if self._attachments is None:\n            self._attachments = self.filestore_cls(self.attachment_cls)\n        return self._attachments\n\n    def attach(self, **kwargs: Any) -> None:\n        if 'content_disposition' not in kwargs:\n            kwargs['content_disposition'] = 'attachment'\n        self.attachments.add(kwargs)\n\n\nclass MessageBuildMixin:\n\n    ROOT_PREAMBLE = 'This is a multi-part message in MIME format.\\n'\n\n    # Header names that contain structured address data (RFC #5322)\n    ADDRESS_HEADERS = set(['from', 'sender', 'reply-to', 'to', 'cc', 'bcc',\n                           'resent-from', 'resent-sender', 'resent-to',\n                           'resent-cc', 'resent-bcc'])\n\n    before_build: Callable[..., Any] | None = None\n    after_build: Callable[..., Any] | None = None\n\n    def encode_header(self, value: str | None) -> str | None:\n        if value:\n            return encode_header_(value, self.charset)\n        else:\n            return value\n\n    def encode_address_header(self, pair: tuple[str | None, str | None] | None) -> str | None:\n        if not pair:\n            return None\n        name, email = pair\n        return formataddr((name or '', email))\n\n    encode_name_header = encode_address_header  # legacy name\n\n    def set_header(self, msg: SafeMIMEMultipart, key: str,\n                   value: str | None, encode: bool = True) -> None:\n\n        if value is None:\n            # TODO: may be remove header here ?\n            return\n\n        if not isinstance(value, str):\n            value = value.decode() if isinstance(value, bytes) else str(value)\n\n        # Prevent header injection\n        if '\\n' in value or '\\r' in value:\n            raise BadHeaderError(\"Header values can't contain newlines (got %r for header %r)\" % (value, key))\n\n        if key.lower() in self.ADDRESS_HEADERS:\n            value = ', '.join(sanitize_address(addr, self.headers_encoding)\n                              for addr in getaddresses((value,)))\n\n        msg[key] = encode and self.encode_header(value) or value\n\n    def _build_root_message(self, message_cls: type | None = None,\n                            **kw: Any) -> SafeMIMEMultipart:\n\n        msg = (message_cls or SafeMIMEMultipart)(**kw)\n\n        if self.policy:\n            msg.policy = self.policy\n\n        msg.preamble = self.ROOT_PREAMBLE\n        self.set_header(msg, 'Date', self.date, encode=False)\n        self.set_header(msg, 'Message-ID', self.message_id, encode=False)\n\n        if self._headers:\n            for (name, value) in self._headers.items():\n                self.set_header(msg, name, value)\n\n        subject = self.subject\n        if subject is not None:\n            self.set_header(msg, 'Subject', subject)\n\n        self.set_header(msg, 'From', self.encode_address_header(self.mail_from), encode=False)\n\n        mail_to = self.mail_to\n        if mail_to:\n            self.set_header(msg, 'To', \", \".join([self.encode_address_header(addr) for addr in mail_to]), encode=False)\n\n        if self.cc:\n            self.set_header(msg, 'Cc', \", \".join([self.encode_address_header(addr) for addr in self.cc]), encode=False)\n\n        if self.reply_to:\n            self.set_header(msg, 'Reply-To', \", \".join([self.encode_address_header(addr) for addr in self.reply_to]), encode=False)\n\n        return msg\n\n    def _build_html_part(self) -> SafeMIMEText | None:\n        text = self.html_body\n        if text:\n            p = SafeMIMEText(text, 'html', charset=self.charset)\n            p.set_charset(self.charset)\n            return p\n        return None\n\n    def _build_text_part(self) -> SafeMIMEText | None:\n        text = self.text_body\n        if text:\n            p = SafeMIMEText(text, 'plain', charset=self.charset)\n            p.set_charset(self.charset)\n            return p\n        return None\n\n    def build_message(self, message_cls: type | None = None) -> SafeMIMEMultipart:\n\n        if self.before_build:\n            self.before_build(self)\n\n        msg = self._build_root_message(message_cls)\n\n        rel = SafeMIMEMultipart('related')\n        msg.attach(rel)\n\n        alt = SafeMIMEMultipart('alternative')\n        rel.attach(alt)\n\n        _text = self._build_text_part()\n        _html = self._build_html_part()\n\n        if not (_html or _text):\n            raise ValueError(\"Message must contain 'html' or 'text'\")\n\n        if _text:\n            alt.attach(_text)\n\n        if _html:\n            alt.attach(_html)\n\n        for f in self.attachments:\n            part = f.mime\n            if part:\n                if f.is_inline:\n                    rel.attach(part)\n                else:\n                    msg.attach(part)\n\n        if self.after_build:\n            self.after_build(self, msg)\n\n        return msg\n\n    _build_message = build_message\n\n    def as_message(self, message_cls: type | None = None) -> SafeMIMEMultipart:\n        msg = self.build_message(message_cls=message_cls)\n        if self._signer:\n            msg = self.sign_message(msg)\n        return msg\n\n    message = as_message\n\n    def as_string(self, message_cls: type | None = None) -> str:\n        \"\"\"\n        Returns message as string.\n\n        Note: this method costs one less message-to-string conversions\n        for dkim in compare to self.as_message().as_string()\n        \"\"\"\n        r = self.build_message(message_cls=message_cls).as_string()\n        if self._signer:\n            r = self.sign_string(r)\n        return r\n\n    def as_bytes(self, message_cls: type | None = None) -> bytes:\n        \"\"\"\n        Returns message as bytes.\n        \"\"\"\n        r = self.build_message(message_cls=message_cls).as_bytes()\n        if self._signer:\n            r = self.sign_bytes(r)\n        return r\n\n\nclass MessageSendMixin:\n\n    smtp_pool_factory = ObjectFactory\n    smtp_cls = SMTPBackend\n\n    @cached_property\n    def smtp_pool(self) -> ObjectFactory:\n        return self.smtp_pool_factory(cls=self.smtp_cls)\n\n    def _prepare_send_params(self,\n                             to: _AddressList = None,\n                             set_mail_to: bool = True,\n                             mail_from: _Address = None,\n                             set_mail_from: bool = False,\n                             render: dict[str, Any] | None = None,\n                             smtp_mail_options: list[str] | None = None,\n                             smtp_rcpt_options: list[str] | None = None) -> dict[str, Any]:\n\n        if render is not None:\n            self.render(**render)\n\n        to_addrs = None\n\n        if to:\n            if set_mail_to:\n                self.set_mail_to(to)\n            else:\n                to_addrs = [a[1] for a in parse_name_and_email_list(to)]\n\n        to_addrs = to_addrs or self.get_recipients_emails()\n\n        if not to_addrs:\n            raise ValueError('No to-addr')\n\n        if mail_from:\n            if set_mail_from:\n                self.set_mail_from(mail_from)\n                from_addr = self._mail_from[1]\n            else:\n                mail_from = parse_name_and_email(mail_from)\n                from_addr = mail_from[1]\n        else:\n            from_addr = self._mail_from[1]\n\n        if not from_addr:\n            raise ValueError('No \"from\" addr')\n\n        return dict(from_addr=from_addr, to_addrs=to_addrs, msg=self,\n                    mail_options=smtp_mail_options, rcpt_options=smtp_rcpt_options)\n\n    def send(self,\n             to: _AddressList = None,\n             set_mail_to: bool = True,\n             mail_from: _Address = None,\n             set_mail_from: bool = False,\n             render: dict[str, Any] | None = None,\n             smtp_mail_options: list[str] | None = None,\n             smtp_rcpt_options: list[str] | None = None,\n             smtp: dict[str, Any] | SMTPBackend | None = None) -> Any:\n\n        if smtp is None:\n            smtp = {'host': 'localhost', 'port': 25, 'timeout': 5}\n\n        if isinstance(smtp, dict):\n            smtp = self.smtp_pool[smtp]\n\n        if not hasattr(smtp, 'sendmail'):\n            raise ValueError(\n                \"smtp must be a dict or an object with method 'sendmail'. got %s\" % type(smtp))\n\n        params = self._prepare_send_params(\n            to=to, set_mail_to=set_mail_to, mail_from=mail_from,\n            set_mail_from=set_mail_from, render=render,\n            smtp_mail_options=smtp_mail_options, smtp_rcpt_options=smtp_rcpt_options)\n\n        return smtp.sendmail(**params)\n\n    async def send_async(self,\n                         to: _AddressList = None,\n                         set_mail_to: bool = True,\n                         mail_from: _Address = None,\n                         set_mail_from: bool = False,\n                         render: dict[str, Any] | None = None,\n                         smtp_mail_options: list[str] | None = None,\n                         smtp_rcpt_options: list[str] | None = None,\n                         smtp: dict[str, Any] | Any | None = None) -> Any:\n\n        try:\n            from .backend.smtp.aio_backend import AsyncSMTPBackend\n        except ImportError:\n            raise ImportError(\n                \"send_async() requires aiosmtplib. \"\n                'Install it with: pip install \"emails[async]\"') from None\n\n        if smtp is None:\n            smtp = {'host': 'localhost', 'port': 25, 'timeout': 5}\n\n        own_backend = False\n        if isinstance(smtp, dict):\n            smtp = AsyncSMTPBackend(**smtp)\n            own_backend = True\n\n        if not hasattr(smtp, 'sendmail'):\n            raise ValueError(\n                \"smtp must be a dict or an AsyncSMTPBackend. got %s\" % type(smtp))\n\n        params = self._prepare_send_params(\n            to=to, set_mail_to=set_mail_to, mail_from=mail_from,\n            set_mail_from=set_mail_from, render=render,\n            smtp_mail_options=smtp_mail_options, smtp_rcpt_options=smtp_rcpt_options)\n\n        try:\n            return await smtp.sendmail(**params)\n        finally:\n            if own_backend:\n                await smtp.close()\n\n\nclass MessageTransformerMixin:\n\n    transformer_cls: type | None = None\n    _transformer: Any = None\n\n    def create_transformer(self, transformer_cls: type | None = None, **kw: Any) -> Any:\n        cls = transformer_cls or self.transformer_cls\n        if cls is None:\n            from .transformer import MessageTransformer  # avoid cyclic import\n            cls = MessageTransformer\n        self._transformer = cls(message=self, **kw)\n        return self._transformer\n\n    def destroy_transformer(self) -> None:\n        self._transformer = None\n\n    @property\n    def transformer(self) -> Any:\n        if self._transformer is None:\n            self.create_transformer()\n        return self._transformer\n\n    def transform(self, **kwargs: Any) -> None:\n        self.transformer.load_and_transform(**kwargs)\n        self.transformer.save()\n\n    def set_html(self, **kw: Any) -> None:\n        # When html set, remove old transformer\n        self.destroy_transformer()\n        BaseMessage.set_html(self, **kw)\n\n\nclass MessageSignMixin:\n\n    signer_cls = DKIMSigner\n    _signer: DKIMSigner | None = None\n\n    def sign(self, **kwargs: Any) -> Message:\n        self._signer = self.signer_cls(**kwargs)\n        return self\n\n    dkim = sign\n\n    def sign_message(self, msg: SafeMIMEMultipart) -> SafeMIMEMultipart:\n        \"\"\"\n        Add sign header to email.Message\n        \"\"\"\n        return self._signer.sign_message(msg)\n\n    def sign_string(self, message_string: str) -> str:\n        \"\"\"\n        Add sign header to message-as-a-string\n        \"\"\"\n        return self._signer.sign_message_string(message_string)\n\n    def sign_bytes(self, message_bytes: bytes) -> bytes:\n        \"\"\"\n        Add sign header to message-as-a-string\n        \"\"\"\n        return self._signer.sign_message_bytes(message_bytes)\n\n\nclass Message(MessageSendMixin, MessageTransformerMixin, MessageSignMixin, MessageBuildMixin, BaseMessage):\n    \"\"\"\n    Email message with:\n    - DKIM signer\n    - smtp send\n    - Message.transformer object\n    \"\"\"\n    pass\n\n\ndef html(**kwargs: Any) -> Message:\n    return Message(**kwargs)\n\n\nclass DjangoMessageProxy:\n\n    \"\"\"\n    Class obsoletes with emails.django_.DjangoMessage\n\n    Class looks like django.core.mail.EmailMessage for standard django email backend.\n\n    Example usage:\n\n        message = emails.Message(html='...', subject='...', mail_from='robot@company.ltd')\n        connection = django.core.mail.get_connection()\n\n        message.set_mail_to('somebody@somewhere.net')\n        connection.send_messages([DjangoMessageProxy(message), ])\n    \"\"\"\n\n    def __init__(self, message: Message, recipients: list[str] | None = None,\n                 context: dict[str, Any] | None = None) -> None:\n        self._message = message\n        self._recipients = recipients\n        self._context = context and context.copy() or {}\n\n        self.from_email: str | None = message.mail_from[1]\n        self.encoding: str = message.charset\n\n    def recipients(self) -> list[str | None]:\n        return self._recipients or [r[1] for r in self._message.mail_to]\n\n    def message(self) -> SafeMIMEMultipart:\n        self._message.render(**self._context)\n        return self._message.message()\n"
  },
  {
    "path": "emails/py.typed",
    "content": ""
  },
  {
    "path": "emails/signers.py",
    "content": "# This module uses dkimpy for DKIM signature\nfrom __future__ import annotations\n\nimport logging\nfrom email.mime.multipart import MIMEMultipart\nfrom typing import IO\n\nimport dkim\nfrom dkim import DKIMException, UnparsableKeyError\n\n\nclass DKIMSigner:\n\n    def __init__(self, selector: str, domain: str, key: str | bytes | IO[bytes] | None = None,\n                 ignore_sign_errors: bool = False, **kwargs: object) -> None:\n\n        self.ignore_sign_errors = ignore_sign_errors\n        self._sign_params = kwargs\n\n        privkey = key or kwargs.pop('privkey', None)  # privkey is legacy synonym for `key`\n\n        if not privkey:\n            raise TypeError(\"DKIMSigner.__init__() requires 'key' argument\")\n\n        if privkey and hasattr(privkey, 'read'):\n            privkey = privkey.read()\n\n        # Normalize to bytes\n        privkey_bytes = privkey if isinstance(privkey, bytes) else str(privkey).encode()\n\n        # Validate key upfront; dkim.sign() re-parses PEM on each call\n        # but the cost is negligible vs the RSA operation (~0ms vs ~2.5ms).\n        try:\n            dkim.crypto.parse_pem_private_key(privkey_bytes)\n        except UnparsableKeyError as exc:\n            raise DKIMException(exc)\n\n        self._sign_params.update({'privkey': privkey_bytes,\n                                  'domain': domain.encode(),\n                                  'selector': selector.encode()})\n\n    def get_sign_string(self, message: bytes) -> bytes | None:\n        try:\n            result: bytes = dkim.sign(message=message, **self._sign_params)\n            return result\n        except DKIMException:\n            if self.ignore_sign_errors:\n                logging.exception('Error signing message')\n            else:\n                raise\n        return None\n\n    def get_sign_bytes(self, message: bytes) -> bytes | None:\n        return self.get_sign_string(message)\n\n    def get_sign_header(self, message: bytes) -> tuple[str, str] | None:\n        s = self.get_sign_string(message)\n        if s:\n            (header, value) = s.decode().split(': ', 1)\n            if value.endswith(\"\\r\\n\"):\n                value = value[:-2]\n            return header, value\n        return None\n\n    def sign_message(self, msg: MIMEMultipart) -> MIMEMultipart:\n        \"\"\"\n        Add DKIM header to email.message\n        \"\"\"\n        dkim_header = self.get_sign_header(msg.as_string().encode())\n        if dkim_header:\n            msg._headers.insert(0, dkim_header)  # type: ignore[attr-defined]\n        return msg\n\n    def sign_message_string(self, message_string: str) -> str:\n        \"\"\"\n        Insert DKIM header to message string\n        \"\"\"\n        s = self.get_sign_string(message_string.encode())\n        if s:\n            return s.decode() + message_string\n        return message_string\n\n    def sign_message_bytes(self, message_bytes: bytes) -> bytes:\n        \"\"\"\n        Insert DKIM header to message bytes\n        \"\"\"\n        s = self.get_sign_bytes(message_bytes)\n        if s:\n            return s + message_bytes\n        return message_bytes\n"
  },
  {
    "path": "emails/store/__init__.py",
    "content": "from .store import MemoryFileStore\nfrom .file import BaseFile, LazyHTTPFile\n"
  },
  {
    "path": "emails/store/file.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom mimetypes import guess_type\nimport puremagic\nfrom email.mime.base import MIMEBase\nfrom email.encoders import encode_base64\nfrom os.path import basename\nfrom typing import Any, IO\n\nimport urllib.parse as urlparse\n\nfrom ..utils import fetch_url, encode_header\n\n\nMIMETYPE_UNKNOWN = 'application/unknown'\n\n\ndef fix_content_type(content_type: str | None, t: str = 'image') -> str:\n    if not content_type:\n        return \"%s/unknown\" % t\n    else:\n        return content_type\n\n\nclass BaseFile:\n\n    \"\"\"\n    Store base \"attachment-file\" information.\n    \"\"\"\n\n    _data: bytes | str | IO[bytes] | None\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        uri and filename are connected properties.\n        if no filename set, filename extracted from uri.\n        if no uri, but filename set, then uri==filename\n        \"\"\"\n        self.uri = kwargs.get('uri', None)\n        self.absolute_url: str | None = kwargs.get('absolute_url', None) or self.uri\n        self.filename = kwargs.get('filename', None)\n        self.data = kwargs.get('data', None)\n        self._mime_type: str | None = kwargs.get('mime_type')\n        self._headers: dict[str, str] = kwargs.get('headers', {})\n        self._content_id: str | None = kwargs.get('content_id')\n        self._content_disposition: str | None = kwargs.get('content_disposition', 'attachment')\n        self.subtype: str | None = kwargs.get('subtype')\n        self.local_loader = kwargs.get('local_loader')\n\n    def as_dict(self, fields: tuple[str, ...] | None = None) -> dict[str, Any]:\n        fields = fields or ('uri', 'absolute_url', 'filename', 'data',\n                            'mime_type', 'content_disposition', 'subtype')\n        return dict([(k, getattr(self, k)) for k in fields])\n\n    def get_data(self) -> bytes | str | None:\n        _data = self._data\n        if isinstance(_data, (str, bytes)):\n            return _data\n        elif _data is None:\n            return None\n        else:\n            return _data.read()\n\n    def set_data(self, value: bytes | str | IO[bytes] | None) -> None:\n        self._data = value\n\n    data = property(get_data, set_data)\n\n    def get_uri(self) -> str | None:\n        _uri = getattr(self, '_uri', None)\n        if _uri is None:\n            _filename = getattr(self, '_filename', None)\n            if _filename:\n                _uri = self._uri = _filename\n        return _uri\n\n    def set_uri(self, value: str | None) -> None:\n        self._uri = value\n\n    uri = property(get_uri, set_uri)\n\n    def get_filename(self) -> str | None:\n        _filename = getattr(self, '_filename', None)\n        if _filename is None:\n            _uri = getattr(self, '_uri', None)\n            if _uri:\n                parsed_path = urlparse.urlparse(_uri)\n                _filename = basename(parsed_path.path)\n                if not _filename:\n                    _filename = str(uuid.uuid4())\n                self._filename = _filename\n        return _filename\n\n    def set_filename(self, value: str | None) -> None:\n        self._filename = value\n\n    filename = property(get_filename, set_filename)\n\n    def get_mime_type(self) -> str:\n        r = getattr(self, '_mime_type', None)\n        if r is None:\n            filename = self.filename\n            if filename:\n                r = self._mime_type = guess_type(filename)[0]\n        if not r:\n            _data = self._data\n            if isinstance(_data, bytes):\n                header = _data\n            elif isinstance(_data, str):\n                header = _data.encode()\n            elif _data is not None:\n                pos = _data.tell()\n                header = _data.read(128)\n                _data.seek(pos)\n            else:\n                header = None\n            if header:\n                try:\n                    r = puremagic.from_string(header, mime=True)\n                except puremagic.PureError:\n                    pass\n        if not r:\n            r = MIMETYPE_UNKNOWN\n        self._mime_type = r\n        return r\n\n    mime_type = property(get_mime_type)\n\n    def get_content_disposition(self) -> str | None:\n        return getattr(self, '_content_disposition', None)\n\n    def set_content_disposition(self, value: str | None) -> None:\n        self._content_disposition = value\n\n    content_disposition = property(get_content_disposition, set_content_disposition)\n\n    @property\n    def is_inline(self):\n        return self.content_disposition == 'inline'\n\n    @is_inline.setter\n    def is_inline(self, value):\n        if bool(value):\n            self.content_disposition = 'inline'\n        else:\n            self.content_disposition = 'attachment'\n\n    @property\n    def content_id(self) -> str | None:\n        if self._content_id is None:\n            self._content_id = self.filename\n        return self._content_id\n\n    @property\n    def mime(self) -> MIMEBase | None:\n        content_disposition = self.content_disposition\n        if content_disposition is None:\n            return None\n        p = getattr(self, '_cached_part', None)\n        if p is None:\n            filename_header = encode_header(self.filename)\n            p = MIMEBase(*self.mime_type.split('/', 1), name=filename_header)\n            data = self.data\n            if isinstance(data, str):\n                payload = data.encode()\n            elif data is not None:\n                payload = bytes(data)\n            else:\n                payload = b''\n            p.set_payload(payload)\n            encode_base64(p)\n            if 'content-disposition' not in self._headers:\n                p.add_header('Content-Disposition', self.content_disposition, filename=filename_header)\n            if content_disposition == 'inline' and 'content-id' not in self._headers:\n                p.add_header('Content-ID', '<%s>' % self.content_id)\n            for (k, v) in self._headers.items():\n                p.add_header(k, v)\n            self._cached_part = p\n        return p\n\n    def reset_mime(self) -> None:\n        self._mime = None\n\n    def fetch(self) -> None:\n        pass\n\n\nclass LazyHTTPFile(BaseFile):\n\n    def __init__(self, requests_args: dict[str, Any] | None = None, **kwargs: Any) -> None:\n        BaseFile.__init__(self, **kwargs)\n        self.requests_args = requests_args\n        self._fetched = False\n\n    def fetch(self) -> None:\n        if (not self._fetched) and self.uri:\n            if self.local_loader:\n                data = self.local_loader[self.uri]\n\n                if data:\n                    self._fetched = True\n                    self._data = data\n                    return\n\n            r = fetch_url(url=self.absolute_url or self.uri, requests_args=self.requests_args)\n            if r.status_code == 200:\n                self._data = r.content\n                self._headers = r.headers\n                self._mime_type = fix_content_type(r.headers.get('content-type'), t='unknown')\n                self._fetched = True\n\n    def get_data(self) -> bytes | str:\n        self.fetch()\n        data = self._data\n        if data is None:\n            return ''\n        if isinstance(data, (str, bytes)):\n            return data\n        return data.read()\n\n    def set_data(self, v: bytes | str | IO[bytes] | None) -> None:\n        self._data = v\n\n    data = property(get_data, set_data)\n\n    @property\n    def mime_type(self) -> str:\n        self.fetch()\n        return self.get_mime_type()\n\n    @property\n    def headers(self) -> dict[str, str]:\n        self.fetch()\n        return self._headers\n"
  },
  {
    "path": "emails/store/store.py",
    "content": "from __future__ import annotations\n\nfrom collections import OrderedDict\nfrom collections.abc import Generator, Iterator\nfrom os.path import splitext\nfrom typing import Any\n\nfrom .file import BaseFile\n\n\nclass FileStore:\n    pass\n\n\nclass MemoryFileStore(FileStore):\n\n    file_cls: type[BaseFile] = BaseFile\n\n    def __init__(self, file_cls: type[BaseFile] | None = None) -> None:\n        if file_cls:\n            self.file_cls = file_cls\n        self._files: OrderedDict[str, BaseFile] = OrderedDict()\n        self._filenames: dict[str, str | None] = {}\n\n    def __contains__(self, k: BaseFile | str | Any) -> bool:\n        if isinstance(k, self.file_cls):\n            return k.uri in self._files\n        elif isinstance(k, str):\n            return k in self._files\n        else:\n            return False\n\n    def keys(self) -> list[str]:\n        return list(self._files.keys())\n\n    def __len__(self) -> int:\n        return len(self._files)\n\n    def as_dict(self) -> Generator[dict[str, Any], None, None]:\n        for d in self._files.values():\n            yield d.as_dict()\n\n    def remove(self, uri: BaseFile | str) -> None:\n        if isinstance(uri, self.file_cls):\n            uri = uri.uri\n\n        assert isinstance(uri, str)\n\n        v = self[uri]\n        if v:\n            filename = v.filename\n            if filename and (filename in self._filenames):\n                del self._filenames[filename]\n            del self._files[uri]\n\n    def unique_filename(self, filename: str | None, uri: str | None = None) -> str | None:\n\n        if filename in self._filenames:\n            n = 1\n            basefilename, ext = splitext(filename)\n\n            while True:\n                n += 1\n                filename = \"%s-%d%s\" % (basefilename, n, ext)\n                if filename not in self._filenames:\n                    break\n\n        if filename is not None:\n            self._filenames[filename] = uri\n\n        return filename\n\n    def add(self, value: BaseFile | dict[str, Any], replace: bool = False) -> BaseFile:\n\n        if isinstance(value, self.file_cls):\n            uri = value.uri\n        elif isinstance(value, dict):\n            value = self.file_cls(**value)\n            uri = value.uri\n        else:\n            raise ValueError(\"Unknown file type: %s\" % type(value))\n\n        if (uri not in self._files) or replace:\n            self.remove(uri)\n            value.filename = self.unique_filename(value.filename, uri=uri)\n            self._files[uri] = value\n\n        return value\n\n    def by_uri(self, uri: str) -> BaseFile | None:\n        return self._files.get(uri, None)\n\n    def by_filename(self, filename: str) -> BaseFile | None:\n        uri = self._filenames.get(filename)\n        if uri:\n            return self.by_uri(uri)\n        return None\n\n    def __getitem__(self, uri: str) -> BaseFile | None:\n        return self.by_uri(uri) or self.by_filename(uri)\n\n    def __iter__(self) -> Iterator[BaseFile]:\n        for k in self._files:\n            yield self._files[k]\n"
  },
  {
    "path": "emails/template/__init__.py",
    "content": "from .jinja_template import JinjaTemplate\nfrom .base import StringTemplate\nfrom .mako_template import MakoTemplate"
  },
  {
    "path": "emails/template/base.py",
    "content": "import string\n\n\nclass BaseTemplate(object):\n\n    def __init__(self, template_text, **kwargs):\n        self.set_template_text(template_text)\n        self.kwargs = kwargs\n\n    def set_template_text(self, template_text):\n        self.template_text = template_text\n        self._template = None\n\n    def render(self, **kwargs):\n        raise NotImplementedError\n\n    def compile_template(self):\n        raise NotImplementedError\n\n    @property\n    def template(self):\n        if self._template is None:\n            self._template = self.compile_template()\n        return self._template\n\n\nclass StringTemplate(BaseTemplate):\n    \"\"\"\n    string.Template based engine.\n    \"\"\"\n    def compile_template(self):\n        safe_substitute = self.kwargs.get('safe_substitute', True)\n        t = string.Template(self.template_text)\n        if safe_substitute:\n            return t.safe_substitute\n        else:\n            return t.substitute\n\n    def render(self, **kwargs):\n        return self.template(**kwargs)"
  },
  {
    "path": "emails/template/jinja_template.py",
    "content": "from .base import BaseTemplate\n\n\nclass JinjaTemplate(BaseTemplate):\n    \"\"\"\n    This template is mostly for demo purposes.\n    You probably want to subclass from it\n    and make more clear environment initialization.\n    \"\"\"\n\n    DEFAULT_JINJA_ENVIRONMENT = {}\n\n    def __init__(self, template_text, environment=None):\n        super(JinjaTemplate, self).__init__(template_text)\n        if environment:\n            self.environment = environment\n        else:\n            if 'jinja2' not in globals():\n                try:\n                    globals()['jinja2'] = __import__('jinja2')\n                except ImportError:\n                    raise ImportError(\n                        \"jinja2 is required for template support. \"\n                        \"Install it with: pip install emails[jinja]\"\n                    )\n            self.environment = jinja2.Environment(**self.DEFAULT_JINJA_ENVIRONMENT)\n\n    def compile_template(self):\n        return self.environment.from_string(self.template_text)\n\n    def render(self, **kwargs):\n        return self.template.render(**kwargs)\n"
  },
  {
    "path": "emails/template/mako_template.py",
    "content": "from .base import BaseTemplate\n\n\nclass MakoTemplate(BaseTemplate):\n\n    def compile_template(self):\n        if 'mako_template' not in globals():\n            globals()['mako_template'] = __import__('mako.template')\n        return mako_template.template.Template(self.template_text)\n\n    def render(self, **kwargs):\n        return self.template.render(**kwargs)\n"
  },
  {
    "path": "emails/testsuite/__init__.py",
    "content": ""
  },
  {
    "path": "emails/testsuite/conftest.py",
    "content": "import logging\nimport datetime\nimport pytest\nimport base64\nimport time\nimport random\nimport sys\nimport platform\n\n\nlogging.basicConfig(level=logging.DEBUG)\nlogger = logging.getLogger()\n\nimport cssutils\ncssutils.log.setLevel(logging.FATAL)\n\n\n@pytest.fixture(scope='module')\ndef django_email_backend(request):\n    from django.conf import settings\n    logger.debug('django_email_backend...')\n    settings.configure(EMAIL_BACKEND='django.core.mail.backends.filebased.EmailBackend',\n                       EMAIL_FILE_PATH='tmp-emails')\n    from django.core.mail import get_connection\n    return get_connection()\n\n"
  },
  {
    "path": "emails/testsuite/django_/test_django_integrations.py",
    "content": "import warnings\nimport pytest\nimport emails\nimport emails.message\n\ndjango = pytest.importorskip(\"django\")\nfrom emails.django import DjangoMessage\n\npytestmark = pytest.mark.django\n\n\ndef test_django_message_proxy(django_email_backend):\n\n    \"\"\"\n    Send message via django email backend.\n    `django_email_backend` defined in conftest.py\n    \"\"\"\n\n    message_params = {'html': '<p>Test from python-emails',\n                      'mail_from': 's@lavr.me',\n                      'mail_to': 's.lavrinenko@gmail.com',\n                      'subject': 'Test from python-emails'}\n    msg = emails.html(**message_params)\n    django_email_backend.send_messages([emails.message.DjangoMessageProxy(msg), ])\n\n\ndef test_django_message_send(django_email_backend):\n\n    message_params = {'html': '<p>Test from python-emails',\n                      'mail_from': 's@lavr.me',\n                      'subject': 'Test from python-emails'}\n    msg = DjangoMessage(**message_params)\n    assert not msg.recipients()\n\n    TO = 'ivan@petrov.com'\n    msg.send(mail_to=TO, set_mail_to=False)\n    assert msg.recipients() == [TO, ]\n    assert not msg.mail_to\n\n    TO = 'x'+TO\n    msg.send(mail_to=TO)\n    assert msg.recipients() == [TO, ]\n    assert msg.mail_to[0][1] == TO\n\n    msg.send(context={'a': 1})\n\n\ndef test_django_message_commons():\n\n    mp = {'html': '<p>Test from python-emails',\n          'mail_from': 's@lavr.me',\n          'mail_to': 'jsmith@company.tld',\n          'charset': 'XXX-Y'}\n    msg = DjangoMessage(**mp)\n\n    assert msg.encoding == mp['charset']\n\n    # --- check recipients()\n\n    assert msg.recipients() == [mp['mail_to'], ]\n\n    msg._set_emails(mail_to='A', set_mail_to=False)\n    assert msg.recipients() == ['A', ]\n    assert msg.mail_to[0][1] == mp['mail_to']\n\n    msg._set_emails(mail_to='a@a.com', set_mail_to=True)\n    assert msg.recipients() == ['a@a.com', ]\n    assert msg.mail_to[0][1] == 'a@a.com'\n\n    # --- check from_email\n\n    assert msg.from_email == mp['mail_from']\n\n    msg._set_emails(mail_from='b@b.com', set_mail_from=False)\n    assert msg.from_email == 'b@b.com'\n    assert msg.mail_from[1] == mp['mail_from']\n\n    msg._set_emails(mail_from='c@c.com', set_mail_from=True)\n    assert msg.from_email == 'c@c.com'\n    assert msg.mail_from[1] == 'c@c.com'\n\n\ndef test_legacy_import():\n    \"\"\"\n    Test legacy django_ module exists and works\n    \"\"\"\n    with warnings.catch_warnings(record=True) as w:\n        from emails.django_ import DjangoMessage as DjangoMessageLegacy\n        assert issubclass(w[-1].category, DeprecationWarning)\n        assert DjangoMessageLegacy == DjangoMessage\n"
  },
  {
    "path": "emails/testsuite/loader/data/html_import/oldornament/oldornament/index.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n<title>SET-3-old-ornament</title>\n\n</head>\n\n<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;\">\n\t<table cellspacing=\"0\" border=\"0\" align=\"center\" cellpadding=\"0\" width=\"100%\">\n\t\t<tr>\n\t\t\t<td valign=\"top\">\n\t\t\t\t<a name=\"top\" style=\"text-decoration: none; color: #cc0000;\"></a>\n\t\t\t\t<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\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"unsubscribe\" align=\"center\" style=\"padding:20px 0\"> <!-- unsubscribe -->\n\t\t\t\t\t\t\t<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 />\n\t\t\t\t\t\t\t\tHaving 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>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"main-td\" style=\"padding: 0 25px;\">\t<!-- introduction and menu box-->\n\t\t\t\t\t\t\t\t<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%\">\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td valign=\"top\" style=\"padding: 10px 12px 0px;\" colspan=\"2\">\n\t\t\t\t\t\t\t\t\t\t\t<table class=\"banner\" cellspacing=\"0\" border=\"0\" style=\"background: #550808; color: #fcfbfa; font-family: 'Times New Roman', Times, serif;\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td style=\"background: #e5ddca;\"><img src=\"images/spacer.gif\" height=\"2\" style=\"display: block; border: none;\" width=\"452\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"right\" style=\"background: #e5ddca;\"><img src=\"images/banner-top.gif\" height=\"2\" style=\"display: block; border: none;\" width=\"90\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td class=\"title\" valign=\"top\" style=\"padding: 0 12px 0;\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img src=\"images/spacer.gif\" width=\"1\" height=\"35\" style=\"display: block; border: none;\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<h1 style=\"padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 60px; line-height: 60px; margin: 0;\">ABC Widgets</h1>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding: 0; color:#fcfbfa; font-family: 'Times New Roman', Times, serif; font-size: 16px; text-transform: uppercase; margin: 0;\"><currentmonthname> NEWSLETTER</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td valign=\"top\" align=\"right\" width=\"90\"><img src=\"images/banner-middle.gif\" height=\"144\" style=\"display: block; border: none;\" width=\"90\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" valign=\"top\" style=\"font-size: 15px; font-style: italic; line-height: 18px; padding:0 35px 12px 12px; width: 329px;\">\n\t\t\t\t\t\t\t\t\t\t\t<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;\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td style=\"padding:25px 0 0;\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding:0; font-family: 'Times New Roman', Times, serif;\"><strong>Dear Simon,</strong></p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding:0; font-family: 'Times New Roman', Times, serif;\">Regards, ABC Widgets</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"menu\" align=\"left\" valign=\"top\" style=\"width: 178px; padding: 0 12px 0 0;\">\n\t\t\t\t\t\t\t\t\t\t\t<table width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\" font-family: 'Times New Roman', Times, serif; font-size: 13px; line-height: 16px;\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td valign=\"top\" align=\"right\"><img src=\"images/banner-bottom.png\" height=\"55\" style=\"display: block; border: none;\" width=\"178\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td valign=\"top\" align=\"left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<ul style=\"margin: 0; padding: 0;\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"footer\" valign=\"top\" colspan=\"2\"><img src=\"images/spacer.gif\" height=\"15\" style=\"display: block; border: none;\" width=\"1\" /></td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"flourish\" valign=\"top\" style=\"padding: 22px 25px;\"><img src=\"images/flourish.png\" height=\"35\" style=\"display: block; border: none;\" width=\"566\" /></td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"main-td\" valign=\"top\" style=\"padding: 0 25px;\">\t<!-- main content -->\n\t\t\t\t\t\t\t\t<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%\">\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" valign=\"top\" style=\"padding: 20px 15px 0 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding:0; margin:0\"><img class=\"divider\" src=\"images/divider.jpg\" height=\"5\" style=\"display: block; border: none;\" width=\"300\" /></p>\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"image\" valign=\"top\" style=\"padding: 20px 10px 15px 0;\">\n\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" border=\"0\" style=\"background: #f0ece2; border: 1px solid #d5d2c9;\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td style=\"padding: 6px;\"><img src=\"images/img01.jpg\" height=\"141\" style=\"display: block; border: none;\" width=\"213\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" valign=\"top\" style=\"padding: 20px 15px 0 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding:0; margin:0\"><img class=\"divider\" src=\"images/divider.jpg\" height=\"5\" style=\"display: block; border: none;\" width=\"300\" /></p>\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"image\" valign=\"top\" style=\"padding: 20px 10px 15px 0;\">\n\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" border=\"0\" style=\"background: #f0ece2; border: 1px solid #d5d2c9;\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td style=\"padding: 6px;\"><img src=\"images/img02.jpg\" height=\"141\" style=\"display: block; border: none;\" width=\"213\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" valign=\"top\" style=\"padding: 20px 15px 0 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t<p style=\"padding:0; margin:0\"><img class=\"divider\" src=\"images/divider.jpg\" height=\"5\" style=\"display: block; border: none;\" width=\"300\" /></p>\n\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"image\" valign=\"top\" style=\"padding: 20px 10px 15px 0;\">\n\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" border=\"0\" style=\"background: #f0ece2; border: 1px solid #d5d2c9;\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td style=\"padding: 6px;\"><img src=\"images/img03.jpg\" height=\"141\" style=\"display: block; border: none;\" width=\"213\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"flourish\" valign=\"top\" style=\"padding: 22px 25px;\"><img src=\"images/flourish.png\" height=\"35\" style=\"display: block; border: none;\" width=\"566\" /></td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"main-td\" valign=\"top\" style=\"padding: 0 25px;\">\t<!-- contact box -->\n\t\t\t\t\t\t\t\t<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%\">\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td colspan=\"3\"><img src=\"images/spacer.gif\" height=\"17\" style=\"display: block; border: none;\" width=\"1\" /></td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" valign=\"top\" style=\" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<p style=\"margin: 0; padding: 0;\">Do you know someone who might be interested in receiving this monthly newsletter?</p>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" align=\"left\" style=\" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<p style=\"margin: 0; padding: 0;\">You're receiving this newsletter because you signed up for the ABC Widget Newsletter.</p>\n\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td class=\"content\" rowspan=\"2\" align=\"left\" valign=\"top\" style=\" font-family: 'Times New Roman', Times, serif; font-size: 12px; padding: 10px 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t<p style=\" font-family: 'Times New Roman', Times, serif; margin: 0; padding: 0;\">123 Some Street<br />\n\t\t\t\t\t\t\t\t\t\t\t\tCity, State<br />\n\t\t\t\t\t\t\t\t\t\t\t\t99999<br />\n\t\t\t\t\t\t\t\t\t\t\t\t(147) 789 7745<br />\n\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"\" style=\"text-decoration: none; color: #cc0000;\">www.abcwidgets.com</a><br />\n\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"mailto:info@abcwidgets.com\" style=\"text-decoration: none; color: #cc0000;\">info@abcwidgets.com</a></p>\n\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<td class=\"content\" valign=\"top\" style=\"font-size: 12px; padding: 10px 12px;\">\n\t\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" border=\"0\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" width=\"57%\"><img src=\"images/arrow.png\" height=\"7\" style=\"display: block; border: none;\" width=\"27\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t\t</table>                    \t\n\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t\t<td class=\"content\" valign=\"top\" style=\"font-size: 12px; padding: 10px 12px;\">                    \n\t\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" border=\"0\" cellpadding=\"0\" width=\"100%\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<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>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" width=\"42%\"><img src=\"images/arrow.png\" height=\"7\" style=\"display: block; border: none;\" width=\"27\" /></td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td class=\"flourish\" valign=\"top\" style=\"padding: 22px 25px;\"><img src=\"images/flourish.png\" height=\"35\" style=\"display: block; border: none;\" width=\"566\" /></td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n</body>\n\n"
  },
  {
    "path": "emails/testsuite/loader/test_helpers.py",
    "content": "\nimport logging; import  cssutils; cssutils.log.setLevel(logging.FATAL)\n\nfrom emails.loader.helpers import (guess_charset, guess_text_charset, decode_text, guess_html_charset, RULES_U)\n\n\ndef test_re_rules():\n    assert RULES_U.re_is_http_equiv.findall('http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"')\n\n\ndef test_guess_charset():\n    assert guess_charset(headers={'content-type': 'text/html; charset=utf-8'}, html='') == 'utf-8'\n\n    assert guess_charset(headers=None, html='<meta  charset=\"xxx-N\"  >') == 'xxx-N'\n\n    html = \"\"\"<html><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\"\"\"\n    assert guess_charset(headers=None, html=html) == 'UTF-8'\n    assert guess_text_charset(html, is_html=True) == 'UTF-8'\n    assert guess_html_charset(html) == 'UTF-8'\n\n    html = \"\"\"Шла Саша по шоссе и сосала сушку\"\"\"\n    assert guess_charset(headers=None, html=html.encode('utf-8')) == 'utf-8'\n\n\ndef test_decode_text():\n\n    import encodings\n\n    def norma_enc(enc):\n        enc_ = encodings.normalize_encoding(enc.lower())\n        enc_ = encodings._aliases.get(enc_) or enc_\n        assert enc_\n        return enc_\n\n    assert decode_text('A')[0] == 'A'\n    assert decode_text(b'A') == ('A', 'ascii')\n\n    for enc in ['utf-8', 'windows-1251', 'cp866']:\n        t = 'Шла Саша по шоссе и сосала сушку. В огороде бузина, в Киеве дядька.'\n        text, guessed_encoding = decode_text(t.encode(enc))\n        print(text, norma_enc(guessed_encoding))\n        assert (text, norma_enc(guessed_encoding)) == (t, norma_enc(enc))\n\n        html = \"\"\"<html><meta http-equiv=\"Content-Type\" content=\"text/html; charset=%s\" />\"\"\" % enc\n        text, guessed_encoding = decode_text(html.encode('utf-8'), is_html=True)\n        print(text, norma_enc(guessed_encoding))\n        assert (text, norma_enc(guessed_encoding)) == (html, norma_enc(enc))\n"
  },
  {
    "path": "emails/testsuite/loader/test_loaders.py",
    "content": "import os\nfrom lxml.etree import XMLSyntaxError\nimport pytest\nfrom requests import ConnectionError, Timeout\n\nimport emails\nimport emails.loader\nimport emails.transformer\nfrom emails.loader.local_store import (MsgLoader, FileSystemLoader, FileNotFound, ZipLoader,\n                                       split_template_path, BaseLoader)\nfrom emails.loader.helpers import guess_charset\nfrom emails.exc import HTTPLoaderError\n\nROOT = os.path.dirname(__file__)\n\nBASE_URL = 'http://lavr.github.io/python-emails/tests/'\n\nOLDORNAMENT_URLS = dict(from_url='campaignmonitor-samples/oldornament/index.html',\n                        from_file='data/html_import/oldornament/oldornament/index.html',\n                        from_zip='data/html_import/oldornament/oldornament.zip')\n\ndef test__from_html():\n\n    with pytest.raises(Exception):\n        emails.loader.from_html(html='')\n\n    assert '-X-' in emails.loader.from_html(html='-X-').html\n\n    # TODO: more tests for from_html func\n\n\ndef load_messages(from_url=None, from_file=None, from_zip=None, from_directory=None, skip_text=False, **kw):\n    # Ususally all loaders loads same data\n    if from_url:\n        print(\"emails.loader.from_url\", BASE_URL + from_url, kw)\n        yield emails.loader.from_url(BASE_URL + from_url, **kw)\n    if from_file:\n        print(\"emails.loader.from_file\", os.path.join(ROOT, from_file), kw)\n        yield emails.loader.from_file(os.path.join(ROOT, from_file), skip_text=skip_text, **kw)\n    if from_directory:\n        print(\"emails.loader.from_directory\", os.path.join(ROOT, from_directory), kw)\n        yield emails.loader.from_directory(os.path.join(ROOT, from_directory), skip_text=skip_text, **kw)\n    if from_zip:\n        print(\"emails.loader.from_zip\", os.path.join(ROOT, from_zip), kw)\n        yield emails.loader.from_zip(open(os.path.join(ROOT, from_zip), 'rb'), skip_text=skip_text, **kw)\n\n\ndef test_loaders():\n\n    def _all_equals(seq):\n        iseq = iter(seq)\n        first = next(iseq)\n        return all(x == first for x in iseq)\n\n    _base_url = os.path.dirname(BASE_URL + OLDORNAMENT_URLS['from_url']) + '/'\n    def _remove_base_url(src, **kw):\n        if src.startswith(_base_url):\n            return src[len(_base_url):]\n        else:\n            return src\n\n    message_params = {'subject': 'X', 'mail_to': 'a@b.net'}\n\n    htmls = []\n\n    for message in load_messages(message_params=message_params, **OLDORNAMENT_URLS):\n        # Check loaded images\n        assert len(message.attachments.keys()) == 13\n\n        valid_filenames = ['arrow.png', 'banner-bottom.png', 'banner-middle.gif', 'banner-top.gif', 'bg-all.jpg',\n                           'bg-content.jpg', 'bg-main.jpg', 'divider.jpg', 'flourish.png', 'img01.jpg', 'img02.jpg',\n                           'img03.jpg', 'spacer.gif']\n        assert sorted([a.filename for a in message.attachments]) == sorted(valid_filenames)\n        print(type(message.attachments))\n        assert len(message.attachments.by_filename('arrow.png').data) == 484\n\n        # Simple html content check\n        assert 'Lorem Ipsum Dolor Sit Amet' in message.html\n\n        # Simple message build check\n        message.as_string()\n\n        # Normalize html and save for later check\n        message.transformer.apply_to_links(_remove_base_url)\n        message.transformer.apply_to_images(_remove_base_url)\n        message.transformer.save()\n        htmls.append(message.html)\n\n    assert _all_equals(htmls)\n\n\ndef test_noindex_loaders():\n\n    with pytest.raises(emails.loader.IndexFileNotFound):\n        emails.loader.from_directory(os.path.join(ROOT, 'data/html_import/no-index/no-index/'))\n\n    with pytest.raises(emails.loader.IndexFileNotFound):\n        emails.loader.from_zip(open(os.path.join(ROOT, 'data/html_import/no-index/no-index.zip'), 'rb'))\n\n\ndef test_loaders_with_params():\n\n    transform_params = [ dict(css_inline=True,\n                            remove_unsafe_tags=True,\n                            set_content_type_meta=True,\n                            load_images=True,\n                            images_inline=True),\n\n                         dict(css_inline=False,\n                              remove_unsafe_tags=False,\n                              set_content_type_meta=False,\n                              load_images=False,\n                              images_inline=False)\n                         ]\n\n    message_params = {'subject': 'X', 'mail_to': 'a@b.net'}\n\n    for tp in transform_params:\n        args = {}\n        args.update(tp)\n        args.update(OLDORNAMENT_URLS)\n        for m in load_messages(requests_params={'timeout': 10},\n                               message_params=message_params,\n                               **args):\n            assert m.subject == message_params['subject']\n            assert m.mail_to[0][1] == message_params['mail_to']\n            for a in m.attachments:\n                assert a.is_inline is True\n\n\ndef test_loader_image_callback():\n\n    checked_images = []\n\n    def check_image_callback(el, **kwargs):\n        if hasattr(el, 'attrib'):\n            checked_images.append(el.attrib['src'])\n        elif hasattr(el, 'uri'):\n            checked_images.append(el.uri)\n        else:\n            assert 0, \"el should be lxml.etree._Element or cssutils.css.value.URIValue\"\n        return False\n\n    for message in load_messages(load_images=check_image_callback, **OLDORNAMENT_URLS):\n        # Check images not loaded\n        assert len(message.attachments.keys()) == 0\n\n    total_images = 0\n    for message in load_messages(**OLDORNAMENT_URLS):\n        # Check loaded images\n        assert len(message.attachments.keys()) == 13\n        total_images += len(message.attachments.keys())\n\n    assert len(checked_images) >= total_images\n\n\ndef test_external_urls():\n\n    # Load some real sites with complicated html and css.\n    # Loader should not throw any exception.\n\n    success = 0\n    for url in [\n                'https://news.ycombinator.com/',\n                'https://www.python.org/'\n                ]:\n        print(\"test_external_urls: url=%s\" % url)\n        try:\n            emails.loader.from_url(url, requests_params={'verify': True})\n            success += 1\n        except (ConnectionError, Timeout):\n            # Nevermind if external site does not respond\n            pass\n        except HTTPLoaderError:\n            # Skip if external site does responds 500\n            pass\n        except SystemError:\n            raise\n\n    assert success  # one of urls should work I hope\n\n\ndef _get_loaders():\n    # All loaders loads same data\n    yield FileSystemLoader(os.path.join(ROOT, \"data/html_import/./oldornament/oldornament\"))\n    yield ZipLoader(open(os.path.join(ROOT, \"data/html_import/oldornament/oldornament.zip\"), 'rb'))\n\n\ndef test_local_store1():\n    for loader in _get_loaders():\n        print(loader)\n        assert isinstance(loader.content('index.html'), str)\n        assert isinstance(loader['index.html'], bytes)\n        assert '<table' in loader.content('index.html')\n        with pytest.raises(FileNotFound):\n            loader.get_file('-nonexistent-file')\n        with pytest.raises(FileNotFound):\n            loader.find_index_file('-nonexistent-file')\n        assert loader.find_index_html()\n        assert not loader.find_index_text()\n        files_list = list(loader.list_files())\n        assert 'images/arrow.png' in files_list\n        assert len(files_list) in [15, 16]\n        # TODO: remove directories from zip loader list_files results\n        assert loader.get_file('./images/img01.jpg') == loader.get_file('images/img01.jpg')\n\n\ndef test_split_template_path():\n\n    with pytest.raises(FileNotFound):\n        split_template_path('../a.git')\n\n\ndef test_base_loader():\n\n    # Prepare simple BaseLoader\n    class TestBaseLoader(BaseLoader):\n        _files = []\n        def list_files(self):\n            return self._files\n        def get_file(self, name):\n            return ('xxx', name) if name in self.list_files() else (None, name)\n\n    l = TestBaseLoader()\n    l._files = ['__MACOSX/.index.html', 'a.html', 'b.html']\n    # Check index file search\n    assert l.find_index_file() == 'a.html'\n\n    # Check .content works\n    assert l.content('a.html') == 'xxx'\n\n    # Raises exception when no html file\n    l._files = ['a.gif', '__MACOSX/.index.html']\n    with pytest.raises(FileNotFound):\n        print(l.find_index_file())\n"
  },
  {
    "path": "emails/testsuite/loader/test_rfc822_loader.py",
    "content": "import glob\nimport email\nimport datetime\nimport os.path\nimport emails.loader\nfrom emails.loader.local_store import MsgLoader\n\nROOT = os.path.dirname(__file__)\n\ndef _get_message():\n    m = emails.loader.from_zip(open(os.path.join(ROOT, \"data/html_import/oldornament/oldornament.zip\"), 'rb'))\n    m.text = 'text'\n    n = len(m.attachments)\n    for i, a in enumerate(m.attachments):\n        a.content_disposition = 'inline' if i < n/2 else 'attachment'\n    m.transformer.synchronize_inline_images()\n    m.transformer.save()\n    #open('oldornament.eml', 'wb').write(m.as_string())\n    return m\n\n\ndef _compare_messages(a, b):\n    assert a.text == b.text\n    assert a.html and a.html == b.html\n    assert len(a.attachments) == len(b.attachments)\n    assert sorted([att.filename for att in a.attachments]) == sorted([att.filename for att in b.attachments])\n    for att in a.attachments:\n        assert att.data == b.attachments.by_filename(att.filename).data\n\n\ndef test_rfc822_loader(**kw):\n    source_message = _get_message()\n    message = emails.loader.from_rfc822(source_message.as_string(), **kw)\n    _compare_messages(message, source_message)\n    assert len(message.attachments.by_filename('arrow.png').data) == 484\n\n\ndef test_msgloader():\n\n    data = {'charset': 'utf-8',\n            'subject': 'Что-то по-русски',\n            'mail_from': ('Максим Иванов', 'ivanov@ya.r'),\n            'mail_to': [('Полина Сергеева', 'polina@mail.r'), ('test', 'test@example.com')],\n            'cc': [('CC User', 'cc@example.com'), ('cc', 'cc.1@example.com')],\n            'html': '<h1>Привет!</h1><p>В первых строках...',\n            'text': 'Привет!\\nВ первых строках...',\n            'headers': {'X-Mailer': 'python-emails',\n                        'Sender': '웃'},\n            'attachments': [{'data': 'X', 'filename': 'Event.ics'},\n                            {'data': 'Y', 'filename': 'Map.png', 'content_disposition': 'inline'},],\n            'message_id': 'message_id'}\n\n    source_message = emails.Message(**data)\n    message = emails.loader.from_rfc822(source_message.as_string(), parse_headers=True)\n\n    loader = message._loader\n\n    assert loader.html == data['html']\n    assert loader.text == data['text']\n\n    assert 'Event.ics' in loader.list_files()\n    assert loader.content('Event.ics') == 'X'\n\n    # check file search by content-id\n    map_cid = \"cid:%s\" % source_message.attachments['Map.png'].content_id\n    assert loader.content(map_cid) == 'Y'\n\n    assert message.mail_to == data['mail_to']\n    assert message.cc == data['cc']\n    assert message.subject == data['subject']\n    print(message._headers)\n    assert message._headers['sender'] == '웃'\n\n    assert message.as_string()\n\n\n\ndef _try_decode(s, charsets=('utf-8', 'koi8-r', 'cp1251')):\n    for charset in charsets:\n        try:\n            return s.decode(charset), charset\n        except UnicodeDecodeError:\n            pass\n    return None, None\n\n\ndef _check_date(s):\n    from dateutil.parser import parse as dateutil_parse\n    if not s:\n        return False\n    try:\n        message_date = dateutil_parse(s)\n    except ValueError:\n        return False\n    return message_date.replace(tzinfo=None) > datetime.datetime(2013, 1, 1)\n\ndef _format_addr(data, one=True):\n    if not data:\n        return None\n    if one:\n        data = [data, ]\n\n    return \",\".join([email.utils.formataddr(pair) for pair in data])\n\n\ndef test_mass_msgloader():\n    import encodings\n    encodings.aliases.aliases['win_1251'] = 'cp1251'  # data-specific\n    ROOT = os.path.dirname(__file__)\n    for filename in glob.glob(os.path.join(ROOT, \"data/msg/*.eml\")):\n        msg, charset = _try_decode(open(filename, 'rb').read())\n        if msg is None:\n            print(\"can not read filename=\", filename)\n            continue\n\n        if not _check_date(email.message_from_string(msg)['date']):\n            continue\n\n        message = emails.loader.from_rfc822(msg=msg, parse_headers=True)\n        if message._headers:\n            print(message._headers)\n        print()\n        print(\"filename:%s\" % filename)\n        print(\"subject:%s\" % message._subject)\n        print(\"from:{0}\".format(_format_addr(message.mail_from, one=True)))\n        print(\"to:{0}\".format(_format_addr(message.mail_to, one=False)))\n        assert message.html or message.text\n    #assert 0\n\n"
  },
  {
    "path": "emails/testsuite/message/__init__.py",
    "content": ""
  },
  {
    "path": "emails/testsuite/message/helpers.py",
    "content": "import os\n\nimport emails\nfrom emails.template import JinjaTemplate\n\nTO_EMAIL = os.environ.get('SMTP_TEST_MAIL_TO') or 'python.emails.test.2@yandex.r'\nFROM_EMAIL = os.environ.get('SMTP_TEST_MAIL_FROM') or 'python-emails@lavr.me'\nROOT = os.path.dirname(__file__)\n\n\ndef common_email_data(**kw):\n    T = JinjaTemplate\n    data = {'charset': 'utf-8',\n            'subject': T('Olá {{name}}'),\n            'mail_from': ('LÖVÅS HÅVET', FROM_EMAIL),\n            'mail_to': ('Pestävä erillään', TO_EMAIL),\n            'html': T('<h1>Olá {{name}}!</h1><p>O Lorem Ipsum é um texto modelo da indústria tipográfica e de impressão.'),\n            'text': T('Olá, {{name}}!\\nO Lorem Ipsum é um texto modelo da indústria tipográfica e de impressão.'),\n            'headers': {'X-Mailer': 'python-emails'},\n            'message_id': emails.MessageID(),\n            'attachments': [\n                {'data': 'Sample text', 'filename': 'κατάσχεση.txt'},\n                {'data': open(os.path.join(ROOT, 'data/pushkin.jpg'), 'rb'), 'filename': 'Пушкин А.С.jpg'}\n            ]}\n    if kw:\n        data.update(kw)\n    return data\n"
  },
  {
    "path": "emails/testsuite/message/test_dkim.py",
    "content": "import pytest\nimport emails\nfrom emails import Message\nfrom io import StringIO\n\nfrom emails.exc import DKIMException\nimport dkim\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom .helpers import common_email_data\n\n\n@pytest.fixture(scope=\"session\")\ndef dkim_keys():\n    \"\"\"Fresh 2048-bit RSA keypair for DKIM tests.\n\n    Returns (private_key_pem, public_key_pem) as bytes.\n    Generated once per test session — RSA keygen is slow (~100 ms).\n    \"\"\"\n    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n    priv_pem = key.private_bytes(\n        encoding=serialization.Encoding.PEM,\n        format=serialization.PrivateFormat.TraditionalOpenSSL,\n        encryption_algorithm=serialization.NoEncryption(),\n    )\n    pub_pem = key.public_key().public_bytes(\n        encoding=serialization.Encoding.PEM,\n        format=serialization.PublicFormat.SubjectPublicKeyInfo,\n    )\n    return priv_pem, pub_pem\n\n\ndef _check_dkim(message, pub_key):\n    def _plain_public_key(s):\n        return b\"\".join([l for l in s.split(b'\\n') if not l.startswith(b'---')])\n    message = message.as_string()\n    o = dkim.DKIM(message=message.encode())\n    return o.verify(dnsfunc=lambda name, **kw: b\"\".join([b\"v=DKIM1; p=\", _plain_public_key(pub_key)]))\n\n\ndef test_dkim(dkim_keys):\n    priv_key, pub_key = dkim_keys\n\n    DKIM_PARAMS = [dict(key=StringIO(priv_key.decode()),\n                        selector='_dkim',\n                        domain='somewhere1.net'),\n\n                   dict(key=priv_key,\n                        selector='_dkim',\n                        domain='somewhere2.net'),\n\n                   # legacy key argument name\n                   dict(privkey=priv_key,\n                        selector='_dkim',\n                        domain='somewhere3.net'),\n                   ]\n\n    for dkimparams in DKIM_PARAMS:\n        message = Message(**common_email_data())\n        message.dkim(**dkimparams)\n        # check DKIM header exist\n        assert message.as_message()['DKIM-Signature']\n        assert 'DKIM-Signature: ' in message.as_string()\n        assert _check_dkim(message, pub_key)\n\n\n\ndef test_dkim_error(dkim_keys):\n    priv_key, _ = dkim_keys\n\n    m = emails.html(**common_email_data())\n\n    # No key\n    with pytest.raises(TypeError):\n        m.dkim(selector='_dkim',\n               domain='somewhere.net',\n               ignore_sign_errors=False)\n\n\n    # Error in invalid key\n    invalid_key = 'X'\n    with pytest.raises(DKIMException):\n        m.dkim(key=invalid_key,\n               selector='_dkim',\n               domain='somewhere.net',\n               ignore_sign_errors=False)\n\n    # Error on invalid dkim parameters\n\n    m.dkim(key=priv_key,\n           selector='_dkim',\n           domain='somewhere.net',\n           include_headers=['To'])\n\n    with pytest.raises(DKIMException):\n        # include_heades must contain 'From'\n        m.as_string()\n\n    # Skip error on ignore_sign_errors=True\n    m.dkim(key=priv_key,\n           selector='_dkim',\n           domain='somewhere.net',\n           ignore_sign_errors=True,\n           include_headers=['To'])\n\n    m.as_string()\n    m.as_message()\n\n\ndef test_dkim_as_bytes(dkim_keys):\n    priv_key, _ = dkim_keys\n    message = Message(**common_email_data())\n    message.dkim(key=priv_key, selector='_dkim', domain='somewhere.net')\n    result = message.as_bytes()\n    assert isinstance(result, bytes)\n    assert b'DKIM-Signature: ' in result\n\n\ndef test_dkim_sign_after_error(dkim_keys):\n    \"\"\"After a sign error with ignore_sign_errors, normal signing still works.\"\"\"\n    priv_key, pub_key = dkim_keys\n\n    # First: sign with invalid include_headers (missing From), error ignored\n    m1 = Message(**common_email_data())\n    m1.dkim(key=priv_key, selector='_dkim', domain='somewhere.net',\n            ignore_sign_errors=True, include_headers=['To'])\n    m1.as_string()  # should not raise\n\n    # Second: normal sign with same key must still work\n    m2 = Message(**common_email_data())\n    m2.dkim(key=priv_key, selector='_dkim', domain='somewhere.net')\n    assert _check_dkim(m2, pub_key)\n\n\ndef test_dkim_sign_twice(dkim_keys):\n\n    # Test #44:\n    # \" if you put the open there and send more than one messages it fails\n    #   (the first works but the next will not if you dont seek(0) the dkim file first)\"\n    # Actually not.\n    priv_key, pub_key = dkim_keys\n\n    message = Message(**common_email_data())\n    message.dkim(key=StringIO(priv_key.decode()), selector='_dkim', domain='somewhere.net')\n    for n in range(2):\n        message.subject = 'Test %s' % n\n        assert _check_dkim(message, pub_key)\n"
  },
  {
    "path": "emails/testsuite/message/test_lazy_gettext.py",
    "content": "import gettext\nfrom emails import Message\nfrom emails.utils import decode_header\n\n\ndef lazy_string(func, string, **variables):\n    from speaklater import make_lazy_string\n    return make_lazy_string(func, string, **variables)\n\n\ndef test_lazy_translated():\n    # prepare translations\n    T = gettext.GNUTranslations()\n    T._catalog = {'invitation': 'invitaci\\xf3n'}\n    _ = T.gettext\n\n    msg = Message(html='...', subject=lazy_string(_, 'invitation'))\n    assert decode_header(msg.as_message()['subject']) == _('invitation')\n\n    msg = Message(html='...', subject='invitaci\\xf3n')\n    assert decode_header(msg.as_message()['subject']) == 'invitaci\\xf3n'\n\n\n\n"
  },
  {
    "path": "emails/testsuite/message/test_message.py",
    "content": "import datetime\nfrom email.utils import parseaddr\nfrom dateutil.parser import parse as dateutil_parse\nimport pytest\n\nimport emails\nfrom emails import Message\nimport emails.exc\nfrom io import StringIO\n\nfrom emails.utils import decode_header, MessageID\nfrom emails.backend.inmemory import InMemoryBackend\n\nfrom .helpers import common_email_data\n\n\ndef test_message_types():\n    m = emails.Message(**common_email_data())\n    assert isinstance(m.as_string(), str)\n\n\ndef test_message_build():\n\n    # Test simple build\n    m = emails.Message(**common_email_data())\n    assert m.as_string()\n\n    # If no html or text - raises ValueError\n    with pytest.raises(ValueError):\n        emails.Message().as_string()\n\n    # Test file-like html and text\n    m = emails.Message(html=StringIO('X'), text=StringIO('Y'))\n    assert m.html == 'X'\n    assert m.text == 'Y'\n\n\ndef test_date():\n\n    # default date is \"current timestamp\"\n    m = emails.Message()\n    assert dateutil_parse(m.date).replace(tzinfo=None) >= datetime.datetime.utcnow() - datetime.timedelta(seconds=3)\n\n    # check date as constant\n    m.date = '2015-01-01'\n    assert m.date == '2015-01-01'\n    assert m.message_date == m.date  # message_date is legacy\n\n    # check date as func with string result\n    m.date = lambda **kw: 'D'\n    assert m.date == 'D'\n\n    # check date as func with time result\n    m.date = lambda **kw: 1426339147.572459\n    assert 'Mar 2015' in m.date\n\n    # check date as func with datetime result\n    m.date = lambda **kw: datetime.datetime(2015, 1, 1)\n    assert m.date.startswith('Thu, 01 Jan 2015 00:00:00')\n\n\ndef test_after_build():\n\n    AFTER_BUILD_HEADER = 'X-After-Build'\n\n    def my_after_build(original_message, built_message):\n        built_message[AFTER_BUILD_HEADER] = '1'\n\n    kwargs = common_email_data()\n    m = emails.Message(**kwargs)\n    m.after_build = my_after_build\n\n    s = m.as_string()\n    print(\"type of message.as_string() is {0}\".format(type(s)))\n    assert AFTER_BUILD_HEADER in s\n\n\ndef test_before_build():\n\n    def my_before_build(message):\n        message.render_data['x-before-build'] = 1\n\n    m = emails.Message(**common_email_data())\n    m.before_build = my_before_build\n\n    s = m.as_string()\n    assert m.render_data['x-before-build'] == 1\n\n\ndef test_sanitize_header():\n    for header, value in (\n            ('subject', 'test\\n'),\n            ('headers', {'X-Header': 'test\\r'}),\n            ):\n        with pytest.raises(emails.exc.BadHeaderError):\n            print('header {0}'.format(header))\n            emails.Message(html='...', **{header: value}).as_message()\n\n\ndef test_headers_not_double_encoded():\n\n    TEXT = '웃'\n\n    m = Message()\n    m.mail_from = (TEXT, 'a@b.c')\n    m.mail_to = (TEXT, 'a@b.c')\n    m.subject = TEXT\n    m.html = '...'\n    msg = m.as_message()\n    assert decode_header(parseaddr(msg['From'])[0]) == TEXT\n    assert decode_header(parseaddr(msg['To'])[0]) == TEXT\n    assert decode_header(msg['Subject']) == TEXT\n\n\ndef test_headers_ascii_encoded():\n    \"\"\"\n    Test we encode To/From header only when it not-ascii\n    \"\"\"\n\n    for text, encoded in (\n        ('웃', '=?utf-8?b?7JuD?='),\n        ('ascii text', 'ascii text'),\n    ):\n        msg = Message(mail_from=(text, 'a@b.c'),\n                      mail_to=(text, 'a@b.c'),\n                      subject=text,\n                      html='...').as_message()\n        assert parseaddr(msg['From'])[0] == encoded\n        assert parseaddr(msg['To'])[0] == encoded\n\n\ndef test_message_addresses():\n\n    m = Message()\n\n    m.mail_from = \"웃 <b@c.d>\"\n    assert m.mail_from == (\"웃\", \"b@c.d\")\n\n    m.mail_from = [\"웃\", \"b@c.d\"]\n    assert m.mail_from == (\"웃\", \"b@c.d\")\n\n    m.mail_to = (\"웃\", \"b@c.d\")\n    assert m.mail_to == [(\"웃\", \"b@c.d\"), ]\n\n    m.mail_to = [(\"웃\", \"b@c.d\"), \"e@f.g\"]\n    assert m.mail_to == [(\"웃\", \"b@c.d\"), (None, \"e@f.g\")]\n\n\ndef test_rfc6532_address():\n    m = Message()\n    m.mail_to = \"anaïs@example.com\"\n    m.html = 'X'\n    assert m.as_string()\n\n\ndef test_message_policy():\n\n    def gen_policy(**kw):\n        import email.policy\n        return email.policy.SMTP.clone(**kw)\n\n    # Generate without policy\n    m1 = emails.Message(**common_email_data())\n    m1.policy = None\n    # Just generate without policy\n    m1.as_string()\n\n    # Generate with policy\n    m1 = emails.Message(**common_email_data())\n    m1.policy = gen_policy(max_line_length=60)\n    # WTF: This check fails.\n    # assert max([len(l) for l in m1.as_string().split(b'\\n')]) <= 60\n    # TODO: another policy checks\n\n\ndef test_message_id():\n\n    params = dict(html='...', mail_from='a@b.c', mail_to='d@e.f')\n\n    # Check message-id not exists by default\n    m = Message(**params)\n    assert not m.as_message()['Message-ID']\n\n    # Check message-id property setter\n    m.message_id = 'ZZZ'\n    assert m.as_message()['Message-ID'] == 'ZZZ'\n\n    # Check message-id exists when argument specified\n    m = Message(message_id=MessageID(), **params)\n    assert m.as_message()['Message-ID']\n\n    m = Message(message_id='XXX', **params)\n    assert m.as_message()['Message-ID'] == 'XXX'\n\n\ndef test_reply_to():\n\n    params = dict(html='...', mail_from='a@b.c', mail_to='to@x.z')\n\n    # Single address\n    m = Message(reply_to='reply@x.z', **params)\n    assert m.as_message()['Reply-To'] == 'reply@x.z'\n\n    # Tuple (name, email)\n    m = Message(reply_to=('Марья', 'maria@example.net'), **params)\n    assert 'maria@example.net' in m.as_message()['Reply-To']\n\n    # Multiple addresses\n    m = Message(reply_to=['a@x.z', 'b@x.z'], **params)\n    assert 'a@x.z' in m.as_message()['Reply-To']\n    assert 'b@x.z' in m.as_message()['Reply-To']\n\n    # No reply-to by default\n    m = Message(**params)\n    assert m.as_message()['Reply-To'] is None\n\n    # Setter\n    m = Message(**params)\n    m.reply_to = 'reply@x.z'\n    assert m.as_message()['Reply-To'] == 'reply@x.z'\n\n\ndef test_several_recipients():\n\n    # Test multiple recipients in \"To\" header\n\n    params = dict(html='...', mail_from='a@b.c')\n\n    m = Message(mail_to=['a@x.z', 'b@x.z'], cc='c@x.z', **params)\n    assert m.as_message()['To'] == 'a@x.z, b@x.z'\n    assert m.as_message()['cc'] == 'c@x.z'\n\n    m = Message(mail_to=[('♡', 'a@x.z'), ('웃', 'b@x.z')], **params)\n    assert m.as_message()['To'] == '=?utf-8?b?4pmh?= <a@x.z>, =?utf-8?b?7JuD?= <b@x.z>'\n\n    # Test sending to several emails\n\n    backend = InMemoryBackend()\n    m = Message(mail_to=[('♡', 'a@x.z'), ('웃', 'b@x.z')], cc=['c@x.z', 'b@x.z'], bcc=['c@x.z', 'd@x.z'], **params)\n    m.send(smtp=backend)\n    for addr in ['a@x.z', 'b@x.z', 'c@x.z', 'd@x.z']:\n        assert len(backend.messages[addr]) == 1\n\n\ndef test_transform():\n    message = Message(html='''<style>h1{color:red}</style><h1>Hello world!</h1>''')\n    message.transform()\n    assert message.html == '<html><head><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\"/></head>' \\\n                           '<body><h1 style=\"color:red\">Hello world!</h1></body></html>'\n"
  },
  {
    "path": "emails/testsuite/message/test_send.py",
    "content": "import time\nimport random\nimport pytest\nimport emails\nimport emails.loader\nfrom emails.backend.smtp import SMTPBackend\n\nfrom .helpers import common_email_data\nfrom emails.testsuite.smtp_servers import get_servers\n\n\ndef get_letters():\n\n    # Test email with attachment\n    URL = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/images/gallery.png'\n    data = common_email_data(subject='Single attachment', attachments=[emails.store.LazyHTTPFile(uri=URL), ])\n    yield emails.html(**data), None\n\n    # Email with render\n    yield emails.html(**common_email_data(subject='Render with name=John')), {'name': 'John'}\n\n    # Email with several inline images\n    url = 'http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html'\n    data = common_email_data(subject='Sample html with inline images')\n    del data['html']\n    yield emails.loader.from_url(url=url, message_params=data, images_inline=True), None\n\n    # Email with utf-8 \"to\"\n    yield emails.Message(**common_email_data(mail_to=\"anaïs@lavr.me\", subject=\"UTF-8 To\")), None\n\n\ndef test_send_letters():\n\n    for m, render in get_letters():\n        for tag, server in get_servers():\n            server.patch_message(m)\n            print(tag, server.params)\n            response = m.send(smtp=server.params, render=render, smtp_mail_options=['smtputf8'])\n            assert response.success\n            # server.sleep()\n\n\n@pytest.mark.e2e\ndef test_send_simple():\n    message = emails.html(**common_email_data(subject='Simple e2e test'))\n    for tag, server in get_servers():\n        server.patch_message(message)\n        response = message.send(smtp=server.params)\n        assert response.success\n\n\n@pytest.mark.e2e\ndef test_send_with_context_manager():\n    for _, server in get_servers():\n        b = SMTPBackend(**server.params)\n        with b as backend:\n            for n in range(2):\n                data = common_email_data(subject='context manager {0}'.format(n))\n                message = emails.html(**data)\n                message = server.patch_message(message)\n                response = message.send(smtp=backend)\n                assert response.success or response.status_code in (421, 451), 'error sending to {0}'.format(server.params)  # gmail not always like test emails\n        assert b._client is None\n"
  },
  {
    "path": "emails/testsuite/message/test_send_async.py",
    "content": "from __future__ import annotations\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nimport emails\nfrom emails.backend.smtp.aio_backend import AsyncSMTPBackend\n\nfrom .helpers import common_email_data\n\n\n@pytest.fixture\ndef mock_smtp():\n    \"\"\"Patch aiosmtplib.SMTP so no real connection is made.\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib.SMTP') as mock_cls:\n        instance = MagicMock()\n        instance.connect = AsyncMock()\n        instance.ehlo = AsyncMock()\n        instance.helo = AsyncMock()\n        instance._ehlo_or_helo_if_needed = AsyncMock()\n        instance.login = AsyncMock()\n        instance.quit = AsyncMock()\n        instance.close = MagicMock()\n        instance.mail = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.rcpt = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.data = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.rset = AsyncMock()\n        instance.is_ehlo_or_helo_needed = True\n        instance.supports_esmtp = True\n        instance.supports_extension = MagicMock(return_value=False)\n        mock_cls.return_value = instance\n        yield instance\n\n\n@pytest.mark.asyncio\nasync def test_send_async_with_dict(mock_smtp):\n    \"\"\"send_async(smtp={...}) creates backend, sends, and closes.\"\"\"\n    msg = emails.html(**common_email_data(subject='Async dict test'))\n    response = await msg.send_async(smtp={'host': 'localhost', 'port': 2525})\n    assert response is not None\n    assert response.success\n    # Backend should have been closed (quit called)\n    mock_smtp.quit.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_send_async_with_backend_object(mock_smtp):\n    \"\"\"send_async(smtp=AsyncSMTPBackend(...)) uses the provided backend.\"\"\"\n    msg = emails.html(**common_email_data(subject='Async backend test'))\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n    response = await msg.send_async(smtp=backend)\n    assert response is not None\n    assert response.success\n    # Backend should NOT have been closed (caller manages lifecycle)\n    mock_smtp.quit.assert_not_awaited()\n    # Clean up\n    await backend.close()\n\n\n@pytest.mark.asyncio\nasync def test_send_async_with_default_smtp(mock_smtp):\n    \"\"\"send_async() without smtp uses default localhost:25.\"\"\"\n    msg = emails.html(**common_email_data(subject='Async default test'))\n    response = await msg.send_async()\n    assert response is not None\n    assert response.success\n\n\ndef test_sync_send_unchanged():\n    \"\"\"message.send() still works the sync path (uses SMTPBackend, not async).\"\"\"\n    msg = emails.html(**common_email_data(subject='Sync unchanged test'))\n\n    mock_backend = MagicMock()\n    mock_response = MagicMock(success=True)\n    mock_backend.sendmail.return_value = mock_response\n\n    response = msg.send(smtp=mock_backend)\n    assert response is mock_response\n    assert response.success\n    mock_backend.sendmail.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_send_async_with_render(mock_smtp):\n    \"\"\"send_async() applies render data before sending.\"\"\"\n    msg = emails.html(**common_email_data(subject='Render test'))\n    response = await msg.send_async(\n        smtp={'host': 'localhost', 'port': 2525},\n        render={'name': 'World'},\n    )\n    assert response is not None\n    assert response.success\n\n\n@pytest.mark.asyncio\nasync def test_send_async_with_to_override(mock_smtp):\n    \"\"\"send_async(to=...) overrides mail_to.\"\"\"\n    msg = emails.html(**common_email_data(subject='To override'))\n    response = await msg.send_async(\n        to='other@example.com',\n        smtp={'host': 'localhost', 'port': 2525},\n    )\n    assert response is not None\n    assert response.success\n    # Verify the override address was used as recipient\n    assert 'other@example.com' in response.to_addrs\n\n\n@pytest.mark.asyncio\nasync def test_send_async_invalid_smtp_type():\n    \"\"\"send_async() raises ValueError for invalid smtp type.\"\"\"\n    msg = emails.html(**common_email_data(subject='Invalid smtp'))\n    with pytest.raises(ValueError, match=\"smtp must be a dict\"):\n        await msg.send_async(smtp=42)\n\n\n@pytest.mark.asyncio\nasync def test_send_async_no_from_raises():\n    \"\"\"send_async() raises when no from address.\"\"\"\n    msg = emails.html(\n        subject='No from',\n        mail_to='to@example.com',\n        html='<p>Hello</p>',\n    )\n    with pytest.raises((ValueError, TypeError)):\n        await msg.send_async(smtp={'host': 'localhost', 'port': 2525})\n\n\n@pytest.mark.asyncio\nasync def test_send_async_closes_on_error(mock_smtp):\n    \"\"\"send_async(smtp={...}) closes backend even if sendmail fails.\"\"\"\n    mock_smtp.mail.side_effect = Exception('send failed')\n    msg = emails.html(**common_email_data(subject='Error close test'))\n\n    with pytest.raises(Exception, match='send failed'):\n        await msg.send_async(\n            smtp={'host': 'localhost', 'port': 2525, 'fail_silently': False},\n        )\n    # Backend should still have been closed\n    mock_smtp.quit.assert_awaited()\n"
  },
  {
    "path": "emails/testsuite/message/test_send_async_e2e.py",
    "content": "\"\"\"\nEnd-to-end async SMTP tests.\n\nThese tests require a running SMTP server (e.g. Mailpit) and are\nskipped unless SMTP_TEST_SETS is set in the environment.  They\nmirror the sync e2e tests in test_send.py but use\n``message.send_async()`` and ``AsyncSMTPBackend``.\n\"\"\"\nfrom __future__ import annotations\n\nimport pytest\n\nimport emails\nfrom emails.backend.smtp.aio_backend import AsyncSMTPBackend\n\nfrom .helpers import common_email_data\nfrom emails.testsuite.smtp_servers import get_servers\n\n\n@pytest.mark.e2e\n@pytest.mark.asyncio\nasync def test_send_async_simple():\n    \"\"\"send_async(smtp={...}) delivers a message through a real SMTP server.\"\"\"\n    message = emails.html(**common_email_data(subject='Async simple e2e test'))\n    for tag, server in get_servers():\n        server.patch_message(message)\n        response = await message.send_async(smtp=server.params)\n        assert response.success\n\n\n@pytest.mark.e2e\n@pytest.mark.asyncio\nasync def test_send_async_with_backend_object():\n    \"\"\"send_async(smtp=AsyncSMTPBackend(...)) delivers a message.\"\"\"\n    for tag, server in get_servers():\n        backend = AsyncSMTPBackend(**server.params)\n        try:\n            message = emails.html(**common_email_data(subject='Async backend obj e2e'))\n            server.patch_message(message)\n            response = await message.send_async(smtp=backend)\n            assert response.success\n        finally:\n            await backend.close()\n\n\n@pytest.mark.e2e\n@pytest.mark.asyncio\nasync def test_send_async_with_context_manager():\n    \"\"\"AsyncSMTPBackend works as an async context manager for multiple sends.\"\"\"\n    for _, server in get_servers():\n        async with AsyncSMTPBackend(**server.params) as backend:\n            for n in range(2):\n                data = common_email_data(subject='async context manager {0}'.format(n))\n                message = emails.html(**data)\n                server.patch_message(message)\n                response = await message.send_async(smtp=backend)\n                assert response.success or response.status_code in (421, 451), \\\n                    'error sending to {0}'.format(server.params)\n        assert backend._client is None\n"
  },
  {
    "path": "emails/testsuite/message/test_template.py",
    "content": "# encode: utf-8\nimport emails\nfrom emails.template import JinjaTemplate, StringTemplate, MakoTemplate\n\n\ndef test_templates_commons():\n    JINJA_TEMPLATE = \"Hello, {{name}}!\"\n    STRING_TEMPLATE = \"Hello, $name!\"\n    MAKO_TEMPLATE = \"Hello, ${name}!\"\n    RESULT = \"Hello, world!\"\n\n    values = {'name': 'world'}\n\n    assert JinjaTemplate(JINJA_TEMPLATE).render(**values) == RESULT\n\n    assert StringTemplate(STRING_TEMPLATE).render(**values) == RESULT\n\n    assert MakoTemplate(MAKO_TEMPLATE).render(**values) == RESULT\n\n\ndef test_render_message_with_template():\n    TEMPLATE = JinjaTemplate('Hello, {{name}}!')\n    V = dict(name='world')\n    RESULT = TEMPLATE.render(**V)\n    assert RESULT == 'Hello, world!'\n\n    msg = emails.html(subject=TEMPLATE)\n    msg.render(**V)\n    assert msg.subject == RESULT\n\n    msg = emails.html(html=TEMPLATE)\n    msg.render(**V)\n    assert msg.html_body == RESULT\n\n    msg = emails.html(text=TEMPLATE)\n    msg.render(**V)\n    assert msg.text_body == RESULT\n"
  },
  {
    "path": "emails/testsuite/smtp/test_aio_client.py",
    "content": "from __future__ import annotations\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom emails.backend.response import SMTPResponse\n\n\n# Helper to build a mock aiosmtplib response\ndef _aio_resp(code: int = 250, message: str = 'OK'):\n    r = MagicMock()\n    r.code = code\n    r.message = message\n    return r\n\n\nclass FakeAsyncSMTPBackend:\n    \"\"\"Minimal stand-in for AsyncSMTPBackend so we can test the client in isolation.\"\"\"\n\n    response_cls = SMTPResponse\n\n    def make_response(self, exception=None):\n        return self.response_cls(backend=self, exception=exception)\n\n\n@pytest.fixture\ndef parent():\n    return FakeAsyncSMTPBackend()\n\n\n@pytest.mark.asyncio\nasync def test_sendmail_success(parent):\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n        mock_smtp_instance.supports_extension = MagicMock(return_value=False)\n        mock_smtp_instance.mail.return_value = _aio_resp(250, 'OK')\n        mock_smtp_instance.rcpt.return_value = _aio_resp(250, 'OK')\n        mock_smtp_instance.data.return_value = _aio_resp(250, 'OK')\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n        await client.initialize()\n\n        response = await client.sendmail(\n            from_addr='sender@example.com',\n            to_addrs=['rcpt@example.com'],\n            msg=b'Subject: test\\r\\n\\r\\nHello',\n        )\n\n        assert response is not None\n        assert isinstance(response, SMTPResponse)\n        assert response.success\n        assert response.status_code == 250\n        assert response.from_addr == 'sender@example.com'\n        assert response.to_addrs == ['rcpt@example.com']\n\n\n@pytest.mark.asyncio\nasync def test_sendmail_empty_to_addrs(parent):\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n\n        response = await client.sendmail(\n            from_addr='sender@example.com',\n            to_addrs=[],\n            msg=b'Subject: test\\r\\n\\r\\nHello',\n        )\n        assert response is None\n\n\n@pytest.mark.asyncio\nasync def test_sendmail_recipient_refused(parent):\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n        mock_smtp_instance.supports_extension = MagicMock(return_value=False)\n        mock_smtp_instance.mail.return_value = _aio_resp(250, 'OK')\n\n        # All recipients refused\n        exc = MagicMock()\n        exc.code = 550\n        exc.message = 'User unknown'\n        mock_aio.SMTPRecipientRefused = type('SMTPRecipientRefused', (Exception,), {})\n        refuse_exc = mock_aio.SMTPRecipientRefused(550, 'User unknown', 'bad@example.com')\n        refuse_exc.code = 550\n        refuse_exc.message = 'User unknown'\n        mock_smtp_instance.rcpt.side_effect = refuse_exc\n        mock_aio.SMTPRecipientsRefused = type('SMTPRecipientsRefused', (Exception,), {})\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n\n        response = await client.sendmail(\n            from_addr='sender@example.com',\n            to_addrs=['bad@example.com'],\n            msg=b'Subject: test\\r\\n\\r\\nHello',\n        )\n\n        assert response is not None\n        assert not response.success\n        assert 'bad@example.com' in response.refused_recipients\n\n\n@pytest.mark.asyncio\nasync def test_sendmail_sender_refused(parent):\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n        mock_smtp_instance.supports_extension = MagicMock(return_value=False)\n\n        # Sender refused via exception\n        mock_aio.SMTPSenderRefused = type('SMTPSenderRefused', (Exception,), {})\n        exc = mock_aio.SMTPSenderRefused(553, 'Sender rejected', 'bad@sender.com')\n        exc.code = 553\n        exc.message = 'Sender rejected'\n        mock_smtp_instance.mail.side_effect = exc\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n\n        response = await client.sendmail(\n            from_addr='bad@sender.com',\n            to_addrs=['rcpt@example.com'],\n            msg=b'Subject: test\\r\\n\\r\\nHello',\n        )\n\n        assert response is not None\n        assert not response.success\n        assert response.error is not None\n\n\n@pytest.mark.asyncio\nasync def test_ssl_and_tls_flags(parent):\n    \"\"\"Test that ssl=True sets use_tls=True and tls=True sets start_tls=True.\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n\n        # ssl=True should pass use_tls=True\n        client_ssl = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=465, ssl=True)\n        call_kwargs = mock_aio.SMTP.call_args\n        assert call_kwargs[1]['use_tls'] is True\n        assert call_kwargs[1]['start_tls'] is False\n\n        # tls=True should pass start_tls=True\n        client_tls = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=587, tls=True)\n        call_kwargs = mock_aio.SMTP.call_args\n        assert call_kwargs[1]['start_tls'] is True\n        assert call_kwargs[1]['use_tls'] is False\n\n\n@pytest.mark.asyncio\nasync def test_quit_handles_disconnect(parent):\n    \"\"\"Test that quit() handles SMTPServerDisconnected gracefully.\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n        mock_aio.SMTPServerDisconnected = type('SMTPServerDisconnected', (Exception,), {})\n        mock_smtp_instance.quit.side_effect = mock_aio.SMTPServerDisconnected()\n        mock_smtp_instance.close = MagicMock()\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n\n        # Should not raise\n        await client.quit()\n        mock_smtp_instance.close.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_initialize_with_login(parent):\n    \"\"\"Test that initialize() performs connect and login when credentials provided.\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(\n            parent=parent, host='localhost', port=587,\n            tls=True, user='testuser', password='testpass',\n        )\n        await client.initialize()\n\n        mock_smtp_instance.connect.assert_awaited_once()\n        mock_smtp_instance.login.assert_awaited_once_with('testuser', 'testpass')\n\n\n@pytest.mark.asyncio\nasync def test_sendmail_string_to_addrs(parent):\n    \"\"\"Test that sendmail handles a string to_addrs (not list).\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib') as mock_aio:\n        mock_smtp_instance = AsyncMock()\n        mock_aio.SMTP.return_value = mock_smtp_instance\n        mock_smtp_instance.supports_extension = MagicMock(return_value=False)\n        mock_smtp_instance.mail.return_value = _aio_resp(250, 'OK')\n        mock_smtp_instance.rcpt.return_value = _aio_resp(250, 'OK')\n        mock_smtp_instance.data.return_value = _aio_resp(250, 'OK')\n\n        from emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n        client = AsyncSMTPClientWithResponse(parent=parent, host='localhost', port=25)\n\n        response = await client.sendmail(\n            from_addr='sender@example.com',\n            to_addrs='rcpt@example.com',  # string, not list\n            msg=b'Subject: test\\r\\n\\r\\nHello',\n        )\n\n        assert response is not None\n        assert response.success\n        assert response.to_addrs == ['rcpt@example.com']\n"
  },
  {
    "path": "emails/testsuite/smtp/test_async_smtp_backend.py",
    "content": "from __future__ import annotations\n\nimport socket\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport aiosmtplib\nimport pytest\nfrom emails.backend.smtp.aio_backend import AsyncSMTPBackend\nfrom emails.backend.smtp.aio_client import AsyncSMTPClientWithResponse\n\n\n@pytest.fixture\ndef mock_msg():\n    msg = MagicMock()\n    msg.as_bytes.return_value = b\"Subject: test\\r\\n\\r\\nHello\"\n    return msg\n\n\n@pytest.fixture\ndef mock_smtp():\n    \"\"\"Patch aiosmtplib.SMTP so no real connection is made.\"\"\"\n    with patch('emails.backend.smtp.aio_client.aiosmtplib.SMTP') as mock_cls:\n        instance = MagicMock()\n        instance.connect = AsyncMock()\n        instance.ehlo = AsyncMock()\n        instance.helo = AsyncMock()\n        instance._ehlo_or_helo_if_needed = AsyncMock()\n        instance.login = AsyncMock()\n        instance.quit = AsyncMock()\n        instance.close = MagicMock()\n        instance.mail = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.rcpt = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.data = AsyncMock(return_value=MagicMock(code=250, message='OK'))\n        instance.rset = AsyncMock()\n        instance.is_ehlo_or_helo_needed = True\n        instance.supports_esmtp = True\n        instance.supports_extension = MagicMock(return_value=False)\n        mock_cls.return_value = instance\n        yield instance\n\n\n@pytest.mark.asyncio\nasync def test_lifecycle_connect_send_close(mock_smtp, mock_msg):\n    \"\"\"Full lifecycle: get_client -> sendmail -> close.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n\n    # get_client creates and initializes the client\n    client = await backend.get_client()\n    assert client is not None\n    assert backend._client is client\n    mock_smtp.connect.assert_awaited_once()\n\n    # sendmail sends the message\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs='c@d.com',\n        msg=mock_msg,\n    )\n    assert response is not None\n    assert response.success\n\n    # close shuts down the connection\n    await backend.close()\n    assert backend._client is None\n    mock_smtp.quit.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_client_reuses_connection(mock_smtp, mock_msg):\n    \"\"\"get_client returns the same client on subsequent calls.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n    client1 = await backend.get_client()\n    client2 = await backend.get_client()\n    assert client1 is client2\n    # connect called only once\n    mock_smtp.connect.assert_awaited_once()\n    await backend.close()\n\n\n@pytest.mark.asyncio\nasync def test_get_client_with_login(mock_smtp):\n    \"\"\"get_client logs in when user/password provided.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525, user='me', password='secret')\n    await backend.get_client()\n    mock_smtp.login.assert_awaited_once_with('me', 'secret')\n    await backend.close()\n\n\n@pytest.mark.asyncio\nasync def test_reconnect_after_disconnect(mock_smtp, mock_msg):\n    \"\"\"After SMTPServerDisconnected during send, backend reconnects and retries.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n\n    # First get_client succeeds, first _send raises disconnect, second _send succeeds\n    call_count = 0\n    original_mail = mock_smtp.mail\n\n    async def mail_side_effect(*args, **kwargs):\n        nonlocal call_count\n        call_count += 1\n        if call_count == 1:\n            raise aiosmtplib.SMTPServerDisconnected('gone')\n        return MagicMock(code=250, message='OK')\n\n    mock_smtp.mail = AsyncMock(side_effect=mail_side_effect)\n\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs='c@d.com',\n        msg=mock_msg,\n    )\n    assert response is not None\n    assert response.success\n    # Should have connected twice (initial + reconnect)\n    assert mock_smtp.connect.await_count == 2\n    await backend.close()\n\n\n@pytest.mark.asyncio\nasync def test_fail_silently_true_on_connect_error(mock_smtp, mock_msg):\n    \"\"\"With fail_silently=True, connection errors return error response without raising.\"\"\"\n    mock_smtp.connect.side_effect = OSError(socket.EAI_NONAME, 'Name not found')\n\n    backend = AsyncSMTPBackend(host='invalid.example', port=2525, fail_silently=True)\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs='c@d.com',\n        msg=mock_msg,\n    )\n    assert response is not None\n    assert not response.success\n    assert response.error is not None\n\n\n@pytest.mark.asyncio\nasync def test_fail_silently_false_raises(mock_smtp, mock_msg):\n    \"\"\"With fail_silently=False, connection errors propagate as exceptions.\"\"\"\n    mock_smtp.connect.side_effect = aiosmtplib.SMTPConnectError('refused')\n\n    backend = AsyncSMTPBackend(host='invalid.example', port=2525, fail_silently=False)\n    with pytest.raises(aiosmtplib.SMTPConnectError):\n        await backend.sendmail(\n            from_addr='a@b.com',\n            to_addrs='c@d.com',\n            msg=mock_msg,\n        )\n\n\n@pytest.mark.asyncio\nasync def test_empty_to_addrs_returns_none(mock_msg):\n    \"\"\"sendmail with empty to_addrs returns None.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs=[],\n        msg=mock_msg,\n    )\n    assert response is None\n\n\n@pytest.mark.asyncio\nasync def test_ssl_tls_mutually_exclusive():\n    \"\"\"Cannot set both ssl and tls.\"\"\"\n    with pytest.raises(ValueError):\n        AsyncSMTPBackend(host='localhost', port=465, ssl=True, tls=True)\n\n\n@pytest.mark.asyncio\nasync def test_context_manager(mock_smtp, mock_msg):\n    \"\"\"AsyncSMTPBackend works as an async context manager.\"\"\"\n    async with AsyncSMTPBackend(host='localhost', port=2525) as backend:\n        client = await backend.get_client()\n        assert client is not None\n    # after exiting, client should be None\n    assert backend._client is None\n\n\n@pytest.mark.asyncio\nasync def test_close_clears_client_on_error(mock_smtp):\n    \"\"\"close() clears the client even if quit raises (when fail_silently=True).\"\"\"\n    mock_smtp.quit.side_effect = aiosmtplib.SMTPServerDisconnected('already gone')\n\n    backend = AsyncSMTPBackend(host='localhost', port=2525, fail_silently=True)\n    await backend.get_client()\n    assert backend._client is not None\n\n    await backend.close()\n    assert backend._client is None\n\n\n@pytest.mark.asyncio\nasync def test_string_to_addrs_converted_to_list(mock_smtp, mock_msg):\n    \"\"\"A single string to_addrs is converted to a list.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525)\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs='c@d.com',\n        msg=mock_msg,\n    )\n    assert response is not None\n    assert response.success\n    await backend.close()\n\n\n@pytest.mark.asyncio\nasync def test_mail_options_passed_through(mock_smtp, mock_msg):\n    \"\"\"mail_options from constructor are used if not overridden in sendmail.\"\"\"\n    backend = AsyncSMTPBackend(host='localhost', port=2525, mail_options=['BODY=8BITMIME'])\n    response = await backend.sendmail(\n        from_addr='a@b.com',\n        to_addrs='c@d.com',\n        msg=mock_msg,\n    )\n    assert response is not None\n    assert response.success\n    # Verify BODY=8BITMIME was passed to the SMTP mail command\n    mail_call_args = mock_smtp.mail.call_args\n    assert 'BODY=8BITMIME' in mail_call_args.kwargs.get('options', [])\n    await backend.close()\n"
  },
  {
    "path": "emails/testsuite/smtp/test_factory.py",
    "content": "import pytest\nfrom emails.backend.factory import ObjectFactory\n\n\ndef test_object_factory():\n    class A:\n        \"\"\" Sample class for testing \"\"\"\n\n        def __init__(self, a, b=None):\n            self.a = a\n            self.b = b\n\n    factory = ObjectFactory(cls=A)\n\n    obj1 = factory[{'a': 1, 'b': 2}]\n    assert isinstance(obj1, A)\n    assert obj1.a == 1\n    assert obj1.b == 2\n\n    obj2 = factory[{'a': 1, 'b': 2}]\n    assert obj2 is obj1\n\n    obj3 = factory[{'a': 100}]\n    assert obj3 is not obj1\n\n    obj4 = factory.invalidate({'a': 100})\n    assert obj3 != obj4\n    assert obj3.a == obj4.a\n\n    with pytest.raises(ValueError):\n        factory[42]\n\n\n"
  },
  {
    "path": "emails/testsuite/smtp/test_smtp_backend.py",
    "content": "\nimport socket\n\nimport pytest\n\nimport emails\nfrom emails.backend.smtp import SMTPBackend\nfrom emails.testsuite.smtp_servers import get_servers\n\nSAMPLE_MESSAGE = {'html': '<p>Test from python-emails',\n                  'text': 'Test from python-emails',\n                  'mail_from': 's@lavr.me',\n                  'mail_to': 'sergei-nko@yandex.r'}\n\n\ndef test_send_to_unknown_host():\n    server = SMTPBackend(host='invalid-server.invalid-domain-42.com', port=2525)\n    response = server.sendmail(to_addrs='s@lavr.me', from_addr='s@lavr.me', msg=emails.html(**SAMPLE_MESSAGE))\n    server.close()\n    assert response.status_code is None\n    assert isinstance(response.error, IOError)\n    assert not response.success\n    # IOError: [Errno 8] nodename nor servname provided, or not known\n    assert response.error.errno == socket.EAI_NONAME\n\n\n@pytest.mark.e2e\ndef test_smtp_send_with_reconnect():\n    \"\"\"\n    Check SMTPBackend.sendmail reconnect\n    \"\"\"\n    for tag, server in get_servers():\n        print(\"-- test_smtp_reconnect: %s\" % server)\n        params = server.params\n        params['fail_silently'] = True\n        message = server.patch_message(emails.html(subject='reconnect test', **SAMPLE_MESSAGE))\n        backend = message.smtp_pool[params]\n        backend.get_client().sock.close()  # simulate disconnect\n        response = message.send(smtp=params)\n        assert response.success or response.status_code in (421, 451)  # gmail don't like test emails\n        server.sleep()\n\n\ndef test_smtp_init_error():\n    \"\"\"\n    Test error when ssl and tls arguments both set\n    \"\"\"\n    with pytest.raises(ValueError):\n        SMTPBackend(host='X', port=25, ssl=True, tls=True)\n\n\ndef test_smtp_empty_sendmail():\n    response = SMTPBackend().sendmail(to_addrs=[], from_addr='a@b.com', msg='')\n    assert not response\n"
  },
  {
    "path": "emails/testsuite/smtp/test_smtp_response.py",
    "content": "from emails.backend.response import SMTPResponse\n\n\ndef test_smtp_response_defaults():\n    r = SMTPResponse()\n    assert r.status_code is None\n    assert r.status_text is None\n    assert r.refused_recipients == {}\n    assert r.esmtp_opts is None\n    assert r.rcpt_options is None\n    assert not r.success\n    assert r.error is None\n\n\ndef test_smtp_response_set_status():\n    r = SMTPResponse()\n    r.set_status('mail', 250, b'OK')\n    assert r.status_code == 250\n    assert r.status_text == b'OK'\n    assert r.last_command == 'mail'\n    assert len(r.responses) == 1\n\n\ndef test_smtp_response_success():\n    r = SMTPResponse()\n    r.set_status('data', 250, b'OK')\n    assert not r.success  # _finished is False\n    r._finished = True\n    assert r.success\n\n\ndef test_smtp_response_refused_recipients():\n    r = SMTPResponse()\n    r.refused_recipients = {}\n    r.refused_recipients['bad@example.com'] = (550, b'User unknown')\n    assert 'bad@example.com' in r.refused_recipients\n    assert r.refused_recipients['bad@example.com'] == (550, b'User unknown')\n\n\ndef test_smtp_response_exception():\n    exc = Exception('connection failed')\n    r = SMTPResponse(exception=exc)\n    assert r.error is exc\n    assert not r.success\n"
  },
  {
    "path": "emails/testsuite/smtp_servers.py",
    "content": "import os\nimport platform\nimport datetime\nimport random\nimport time\n\nDEFAULT_FROM = os.environ.get('SMTP_TEST_FROM_EMAIL') or 'python-emails@lavr.me'\nSUBJECT_SUFFIX = os.environ.get('SMTP_TEST_SUBJECT_SUFFIX')\n\n\ndef as_bool(value, default=False):\n    if value is None:\n        return default\n    return value.lower() in ('1', 'yes', 'true', 'on')\n\n\n\"\"\"\nTake environment variables if exists and send test letter\n\nSMTP_TEST_SETS=GMAIL,OUTLOOK,YAMAIL\n\nSMTP_TEST_GMAIL_TO=somebody@gmail.com\nSMTP_TEST_GMAIL_USER=myuser\nSMTP_TEST_GMAIL_PASSWORD=mypassword\nSMTP_TEST_GMAIL_WITH_TLS=true\nSMTP_TEST_GMAIL_WITHOUT_TLS=false\nSMTP_TEST_GMAIL_HOST=alt1.gmail-smtp-in.l.google.com\nSMTP_TEST_GMAIL_PORT=25\n\n...\n\n\"\"\"\n\n\ndef smtp_server_from_env(name='GMAIL'):\n\n    def _var(param, default=None):\n        v = os.environ.get('SMTP_TEST_{}_{}'.format(name, param), default)\n        return v\n\n    def _valid_smtp(data):\n        return data['host']\n\n    smtp_info = dict(\n        from_email=_var(\"FROM\", default=DEFAULT_FROM),\n        to_email=_var(\"TO\"),\n        host=_var('HOST'),\n        port=_var('PORT', default=25),\n        user=_var('USER'),\n        password=_var('PASSWORD')\n    )\n\n    if _valid_smtp(smtp_info):\n\n        if as_bool(_var('WITH_TLS')):\n            smtp_info['tls'] = True\n            sys_name = '{}_WITH_TLS'.format(name)\n            yield sys_name, smtp_info\n\n        if as_bool(_var('WITHOUT_TLS')):\n            smtp_info['tls'] = False\n            sys_name = '{}_WITHOUT_TLS'.format(name)\n            yield sys_name, smtp_info\n\n\nclass SMTPTestParams(object):\n\n    subject_prefix = '[python-emails]'\n\n    def __init__(self, from_email=None, to_email=None, defaults=None, **kw):\n        params = {'fail_silently': False, 'debug': 1, 'timeout': 25}\n        params.update(defaults or {})\n        params.update(kw)\n        self.params = params\n        self.from_email = from_email\n        self.to_email = to_email\n\n    def patch_message(self, message):\n        \"\"\"\n        Some SMTP requires from and to emails\n        \"\"\"\n\n        if self.from_email:\n            message.mail_from = (message.mail_from[0], self.from_email)\n\n        if self.to_email:\n            message.mail_to = self.to_email\n\n        # TODO: this code breaks template in subject; fix it\n        if not message.subject.startswith(self.subject_prefix):\n            message.subject = \" \".join([self.subject_prefix, message.subject,\n                                        '// %s' % SUBJECT_SUFFIX])\n\n        message._headers['X-Test-Date'] = datetime.datetime.utcnow().isoformat()\n        message._headers['X-Python-Version'] = \"%s/%s\" % (platform.python_version(), platform.platform())\n        message._headers['X-Build-Data'] = SUBJECT_SUFFIX\n\n        return message\n\n    def __str__(self):\n        return 'SMTPTestParams({user}@{host}:{port})'.format(host=self.params.get('host'),\n                                                              port=self.params.get('port'),\n                                                              user=self.params.get('user', ''))\n\n    def sleep(self):\n        if 'mailtrap' in self.params.get('host', ''):\n            t = 2 + random.randint(0, 2)\n        else:\n            t = 0.5\n        time.sleep(t)\n\n\ndef get_servers():\n    names = os.environ.get('SMTP_TEST_SETS', None)\n    if names:\n        for name in names.split(','):\n            for sys_name, params in smtp_server_from_env(name):\n                yield sys_name, SMTPTestParams(**params)\n\n"
  },
  {
    "path": "emails/testsuite/store/test_store.py",
    "content": "from io import BytesIO\n\nimport pytest\nimport emails\nimport emails.store\nfrom emails.store.file import BaseFile, fix_content_type\n\n\ndef test_fix_content_type():\n    assert fix_content_type('x') == 'x'\n    assert fix_content_type('') == 'image/unknown'\n\n\ndef test_lazy_http():\n    IMG_URL = 'http://lavr.github.io/python-emails/tests/python-logo.gif'\n    f = emails.store.LazyHTTPFile(uri=IMG_URL)\n    assert f.filename == 'python-logo.gif'\n    assert f.content_disposition == 'attachment'\n    assert len(f.data) == 2549\n\n\ndef test_attachment_headers():\n    f = emails.store.BaseFile(data='x', filename='1.txt', headers={'X-Header': 'X'})\n    part = f.mime.as_string()\n    assert 'X-Header: X' in part\n\n\ndef test_store_commons():\n    FILES = [{'data': 'aaa', 'filename': 'aaa.txt'}, {'data': 'bbb', 'filename': 'bbb.txt'}, ]\n    store = emails.store.MemoryFileStore()\n    [store.add(_) for _ in FILES]\n    for i, stored_file in enumerate(store):\n        orig_file = FILES[i]\n        for (k, v) in orig_file.items():\n            assert v == getattr(stored_file, k)\n\n\ndef test_store_unique_name():\n    store = emails.store.MemoryFileStore()\n    f1 = store.add({'uri': '/a/c.gif'})\n    assert f1.filename == 'c.gif'\n    f2 = store.add({'uri': '/a/b/c.gif'})\n    assert f2.filename == 'c-2.gif'\n    assert f1.content_id != f2.content_id\n    f3 = store.add({'uri': '/a/c/c.gif'})\n    assert f3.filename == 'c-3.gif'\n    assert f1.content_id != f3.content_id\n    assert f2.content_id != f3.content_id\n\n\ndef test_get_data_str():\n    f = BaseFile(data='hello')\n    assert f.data == 'hello'\n\n\ndef test_get_data_bytes():\n    f = BaseFile(data=b'hello')\n    assert f.data == b'hello'\n\n\ndef test_get_data_filelike():\n    f = BaseFile(data=BytesIO(b'hello'))\n    assert f.data == b'hello'\n\n\ndef test_get_data_none():\n    f = BaseFile()\n    assert f.data is None\n\n\ndef test_mime_type_from_content():\n    # PNG magic bytes, no file extension\n    png_header = (b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR'\n                  b'\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x08\\x02'\n                  b'\\x00\\x00\\x00\\x90wS\\xde')\n    f = BaseFile(data=png_header, filename='image_no_ext')\n    assert f.mime_type == 'image/png'\n\n    # JPEG magic bytes, no file extension\n    jpeg_header = b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF'\n    f = BaseFile(data=jpeg_header, filename='photo')\n    assert f.mime_type == 'image/jpeg'\n\n    # Unknown bytes, no extension — should fall back to unknown\n    f = BaseFile(data=b'\\x00\\x01\\x02\\x03', filename='mystery')\n    assert f.mime_type == 'application/unknown'\n\n    # Extension still takes priority\n    f = BaseFile(data=png_header, filename='image.gif')\n    assert f.mime_type == 'image/gif'\n\n    # File-like data: mime detected without exhausting stream\n    f = BaseFile(data=BytesIO(png_header), filename='no_ext')\n    assert f.mime_type == 'image/png'\n    assert f.data == png_header  # stream not consumed by mime detection\n\n\ndef test_store_commons2():\n    store = emails.store.MemoryFileStore()\n    f1 = store.add({'uri': '/a/c.gif'})\n    assert f1.filename\n    assert f1.content_id\n    assert f1 in store and f1.uri in store  # tests __contains__\n    assert len(store) == 1  # tests __len__\n    assert len(list(store.as_dict())) == 1\n    with pytest.raises(ValueError):\n        store.add(\"X\")\n    store.remove(f1)\n    assert f1 not in store\n    assert len(store) == 0\n"
  },
  {
    "path": "emails/testsuite/test_templates.py",
    "content": "import pytest\nfrom emails.template import MakoTemplate, StringTemplate, JinjaTemplate\nfrom emails.template.base import BaseTemplate\n\n\ndef test_template_cache():\n    t = BaseTemplate(\"A\")\n    t._template = 'XXX'\n    t.set_template_text('B')\n    assert t._template is None\n    assert t.template_text == 'B'\n\n\ndef test_templates_basics():\n    valid_result = \"Hello, world!\"\n    for cls, tmpl in ((StringTemplate, \"Hello, ${name}!\"),\n                      (MakoTemplate, \"Hello, ${name}!\"),\n                      (JinjaTemplate, \"Hello, {{name}}!\")):\n        assert cls(tmpl).render(name='world') == valid_result\n\n\ndef test_string_template_safe_subst():\n    StringTemplate(\"${n}${m}\").render(n='42') == \"42\"\n    with pytest.raises(KeyError):\n        StringTemplate(\"${n}${m}\", safe_substitute=False).render(n=42) == \"42\"\n"
  },
  {
    "path": "emails/testsuite/test_utils.py",
    "content": "import pytest\nimport datetime\nimport time\nfrom emails.utils import (parse_name_and_email, encode_header, decode_header, sanitize_address, fetch_url,\n                          MessageID, format_date_header, parse_name_and_email_list, sanitize_email)\nfrom emails.exc import HTTPLoaderError\n\n\ndef test_parse_name_and_email():\n    assert parse_name_and_email('john@smith.me') == (None, 'john@smith.me')\n    assert parse_name_and_email('\"John Smith\" <john@smith.me>') == \\\n           ('John Smith', 'john@smith.me')\n    assert parse_name_and_email(['John Smith', 'john@smith.me']) == \\\n           ('John Smith', 'john@smith.me')\n    with pytest.raises(ValueError):\n        parse_name_and_email(1)\n    with pytest.raises(ValueError):\n        parse_name_and_email([42,])\n\n\ndef test_parse_name_and_list():\n    assert parse_name_and_email_list(['a@b.c', 'd@e.f']) == [(None, 'a@b.c'), (None, 'd@e.f')]\n    assert parse_name_and_email_list(('a@b.c', 'd@e.f')) == [('a@b.c', 'd@e.f'), ]\n    assert parse_name_and_email_list(['a@b.c']) == [(None, 'a@b.c')]\n    assert parse_name_and_email_list(\"♤ <a@b.c>\") == [(\"♤\", 'a@b.c'), ]\n\n\ndef test_header_encode():\n    v = 'Мама мыла раму. ' * 30\n    assert decode_header(encode_header(v)).strip() == v.strip()\n    assert encode_header(1) == 1\n\n\ndef test_sanitize_address():\n    assert sanitize_address('a <b>') == 'a <b>'\n    assert sanitize_address('a@b.d') == 'a@b.d'\n    assert sanitize_address('x y <a@b.d>') == 'x y <a@b.d>'\n    assert sanitize_address('♤ <a@b.d>') == '=?utf-8?b?4pmk?= <a@b.d>'\n    assert sanitize_address('a@♤.d') == 'a@xn--f6h.d'\n\n\ndef test_sanitize_email():\n    assert sanitize_email('a@♤.d') == 'a@xn--f6h.d'\n\n\ndef test_fetch_url():\n    fetch_url('http://google.com')\n    with pytest.raises(HTTPLoaderError):\n        fetch_url('http://google.com/nonexistent-no-page')\n\n\ndef test_message_id():\n    # Test message-id generate\n    assert MessageID()()\n    assert '___xxx___' in MessageID(idstring='___xxx___')()\n    assert '___yyy___' in MessageID(domain='___yyy___')()\n\n    # Test message-id generate\n    _ids = set()\n    gen = MessageID()\n    for _ in range(100):\n        _id = gen()\n        if len(_ids) == 1:\n            _ids.add(_id)\n            continue\n        else:\n            assert _id not in _ids\n            _ids.add(_id)\n\n\ndef test_url_fix():\n    # Check url with unicode and spaces\n    r = fetch_url('http://lavr.github.io/python-emails/tests/url-fix/Пушкин А.С.jpg')\n    assert len(r.content) == 12910\n\n\ndef test_format_date():\n    current_year = str(datetime.datetime.now().year)\n    assert current_year in format_date_header(None)\n    assert current_year in format_date_header(datetime.datetime.now())\n    assert current_year in format_date_header(time.time())\n    assert 'X' == format_date_header('X')\n"
  },
  {
    "path": "emails/testsuite/transformer/data/premailer_load/style.css",
    "content": "a {color: #000000;}"
  },
  {
    "path": "emails/testsuite/transformer/test_parser.py",
    "content": "from emails.transformer import HTMLParser\n\n\ndef test_parser_inputs():\n\n    def _cleaned_body(s):\n        for el in ('html', 'body', 'head'):\n            s = s.replace('<%s>' % el, '').replace('</%s>' % el, '').replace('<%s/>' % el, '')\n        return s\n\n    # This is a fixation of de-facto rendering results\n\n    for html, result in (\n            (\"<html><!-- comment -->\", \"<!-- comment -->\"),\n            (\"<a href='[]&'>_</a>\", '<a href=\"[]&amp;\">_</a>'),\n            ('<p>a\\r\\n', '<p>a</p>')\n    ):\n        for parser in [HTMLParser(html=html, method='html'),\n                       HTMLParser(html=html, method='html5')]:\n            r = parser.to_string()\n            print(\"html=\", html.__repr__(), \"result=\", r.__repr__(), sep='')\n            assert _cleaned_body(r) == result\n\n\ndef test_breaking_title():\n    # xml-styled <title/> breaks html rendering, we should remove it (#43)\n    assert '<title/>' not in HTMLParser(html=\"<title></title>\").to_string()\n"
  },
  {
    "path": "emails/testsuite/transformer/test_transformer.py",
    "content": "import os.path\nimport emails.loader\nfrom emails.loader.local_store import FileSystemLoader, BaseLoader\nfrom emails.template import JinjaTemplate, StringTemplate, MakoTemplate\nfrom emails.transformer import Transformer, LocalPremailer, BaseTransformer, HTMLParser\n\n\ndef test_image_apply():\n\n    pairs = [\n        (\"\"\"<div style=\"background: url(3.png);\">x</div>\"\"\",\n         \"\"\"<div style=\"background: url(A/3.png)\">x</div>\"\"\"),\n\n        (\"\"\"<img src=\"4.png\"/>\"\"\",\n         \"\"\"<img src=\"A/4.png\"/>\"\"\"),\n\n        (\"\"\"<table background=\"5.png\"/>\"\"\",\n         \"\"\"<table background=\"A/5.png\"/>\"\"\")\n    ]\n\n    def func(uri, **kw):\n        return \"A/\"+uri\n\n    for before, after in pairs:\n        t = Transformer(html=before)\n        t.apply_to_images(func)\n        assert after in t.to_string()\n\n\ndef test_html5_transform():\n    assert Transformer(html=\"<a><table/></a>\", method=\"html\").to_string() == '<html><body><a/><table/></body></html>'\n    assert Transformer(html=\"<a><table/></a>\", method=\"html5\").to_string() == '<html><head/><body><a><table/></a></body></html>'\n\n\ndef test_entity_13():\n    assert \"<div>x\\n</div>\" in Transformer(html=\"<div>x\\r\\n</div>\").to_string()\n\n\ndef test_link_apply():\n\n    pairs = [\n        (\"\"\"<a href=\"1\">_</a>\"\"\",\n         \"\"\"<a href=\"A/1\">_</a>\"\"\"),\n    ]\n\n    def func(uri, **kw):\n        return \"A/\"+uri\n\n    for before, after in pairs:\n        t = Transformer(html=before)\n        t.apply_to_links(func)\n        assert after in t.to_string()\n\n\ndef test_tag_attribute():\n\n    m1 = emails.loader.from_string(html=\"\"\"<img src=\"1.jpg\">\"\"\")\n    assert len(m1.attachments.keys()) == 1\n    assert m1.attachments['1.jpg'].content_disposition != \"inline\"\n\n    m2 = emails.loader.from_string(html=\"\"\"<img src=\"1.jpg\" data-emails=\"ignore\">\"\"\")\n    assert len(m2.attachments.keys()) == 0\n\n    m3 = emails.loader.from_string(html=\"\"\"<img src=\"1.jpg\" data-emails=\"inline\">\"\"\")\n    assert len(m3.attachments.keys()) == 1\n    assert m3.attachments['1.jpg'].content_disposition == \"inline\"\n\n\ndef test_data_uri_preserved():\n    DATA_URI = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n\n    # data URIs in img src should be left untouched\n    html = '<img src=\"%s\"/>' % DATA_URI\n    m = emails.loader.from_string(html=html)\n    assert len(m.attachments.keys()) == 0\n    assert DATA_URI in m.html\n\n    # data URIs in css url() should be left untouched\n    html = '<div style=\"background: url(%s);\">x</div>' % DATA_URI\n    m = emails.loader.from_string(html=html)\n    assert len(m.attachments.keys()) == 0\n\n    # data URIs in background attribute should be left untouched\n    html = '<table background=\"%s\"/>' % DATA_URI\n    m = emails.loader.from_string(html=html)\n    assert len(m.attachments.keys()) == 0\n\n    # mixed-case scheme should also be preserved\n    mixed = DATA_URI.replace('data:', 'Data:')\n    html = '<img src=\"%s\"/>' % mixed\n    m = emails.loader.from_string(html=html)\n    assert len(m.attachments.keys()) == 0\n\n\nROOT = os.path.dirname(__file__)\n\ndef test_local_premailer():\n    local_loader = FileSystemLoader(os.path.join(ROOT, \"data/premailer_load\"))\n    lp = LocalPremailer(html='<link href=\"style.css\" rel=\"stylesheet\" /><a href=\"#\">', local_loader=local_loader)\n    assert '<a href=\"#\" style=\"color:#000\">' in lp.transform()\n\n\ndef test_add_content_type_meta():\n    t = Transformer(html=\"<div></div>\")\n    t.premailer.transform()\n    assert type(t.html) == type(t.to_string())\n    t.add_content_type_meta(content_type=\"text/html\", charset=\"utf-16\")\n    assert 'content=\"text/html; charset=utf-16\"' in t.to_string()\n\n\ndef test_image_inline():\n\n    class SimpleLoader(BaseLoader):\n        def __init__(self, data):\n            self.__data = data\n        def list_files(self):\n            return self.__data.keys()\n        def get_file(self, name):\n            return self.__data.get(name, None), name\n\n    t = Transformer(html=\"<div><img src='a.gif'></div>\", local_loader=SimpleLoader(data={'a.gif': 'xxx'}))\n    t.load_and_transform()\n\n    t.attachment_store['a.gif'].content_disposition = 'inline'\n    t.synchronize_inline_images()\n    t.save()\n    assert \"cid:a.gif\" in t.html\n\n    t.attachment_store['a.gif'].content_disposition = None\n    t.synchronize_inline_images()\n    t.save()\n    assert '<img src=\"a.gif\"' in t.html\n\n    # test inline image in css\n    t = Transformer(html=\"<div style='background:url(a.gif);'></div>\", local_loader=SimpleLoader(data={'a.gif': 'xxx'}))\n    t.load_and_transform()\n    t.attachment_store['a.gif'].content_disposition = 'inline'\n    t.synchronize_inline_images()\n    t.save()\n    assert \"url(cid:a.gif)\" in t.html\n\n\n\ndef test_absolute_url():\n    t = Transformer(html=\"\", base_url=\"https://host1.tld/a/b\")\n    assert t.get_absolute_url('c.gif') == 'https://host1.tld/a/b/c.gif'\n    assert t.get_absolute_url('/c.gif') == 'https://host1.tld/c.gif'\n    assert t.get_absolute_url('//host2.tld/x/y.png') == 'https://host2.tld/x/y.png'\n\n\ndef test_html_parser_with_templates():\n    for _, html in (\n            (JinjaTemplate, '{{ name }} <a href=\"{{ link }}\">_</a>'),\n            (StringTemplate, '${name} <a href=\"${link}\">_</a>'),\n            (MakoTemplate, '${name} <a href=\"${link}\">_</a>')\n    ):\n        # Test that html parser doesn't break template code\n        t = HTMLParser(html=html, method='html')\n        assert html in t.to_string()\n\n\ndef test_template_transformer():\n    \"\"\"\n    Test that transformer works with templates\n    \"\"\"\n    for template_cls, tmpl in (\n            (JinjaTemplate, '{{ name }} <a href=\"{{ link }}\">_</a>'),\n            (StringTemplate, '${name} <a href=\"${link}\">_</a>'),\n            (MakoTemplate, '${name} <a href=\"${link}\">_</a>')\n    ):\n        m = emails.Message(html=template_cls(tmpl))\n        m.transformer.premailer.transform()\n        m.transformer.save()\n        m.render(name='NAME', link='LINK')\n        assert '<html>' in m.html_body\n        assert 'NAME' in m.html_body\n        assert '<a href=\"LINK\">_</a>' in m.html_body\n"
  },
  {
    "path": "emails/transformer.py",
    "content": "\nimport functools\nimport logging\nimport posixpath\nimport re\nimport warnings\n\nfrom cssutils import CSSParser\nfrom lxml import etree\nfrom premailer import Premailer\nfrom premailer.premailer import ExternalNotFoundError\n\nimport urllib.parse as urlparse\n\nfrom .loader.local_store import FileNotFound\nfrom .store import MemoryFileStore, LazyHTTPFile\nfrom .template.base import BaseTemplate\n\n\nclass LocalPremailer(Premailer):\n\n    def __init__(self, html, local_loader=None, attribute_name=None, **kw):\n        if 'preserve_internal_links' not in kw:\n            kw['preserve_internal_links'] = True\n        self.local_loader = local_loader\n        if attribute_name:\n            self.attribute_name = attribute_name\n        super(LocalPremailer, self).__init__(html=html, **kw)\n\n    def _load_external(self, url):\n        \"\"\"\n        loads an external stylesheet from a remote url or local store\n        \"\"\"\n        if url.startswith('//'):\n            # then we have to rely on the base_url\n            if self.base_url and 'https://' in self.base_url:\n                url = 'https:' + url\n            else:\n                url = 'http:' + url\n\n        if url.startswith('http://') or url.startswith('https://'):\n            content = self._load_external_url(url)\n        else:\n            content = None\n\n            if self.local_loader:\n                try:\n                    content = self.local_loader[url]\n                except FileNotFound:\n                    content = None\n\n            if content is None:\n                if self.base_url:\n                    return self._load_external(urlparse.urljoin(self.base_url, url))\n                else:\n                    raise ExternalNotFoundError(url)\n\n        return content\n\n\nclass HTMLParser(object):\n    _cdata_regex = re.compile(r'\\<\\!\\[CDATA\\[(.*?)\\]\\]\\>', re.DOTALL)\n    _xml_title_regex = re.compile(r'\\<title(.*?)\\/\\>', re.IGNORECASE)\n    default_parser_method = \"html\"\n    default_output_method = \"xml\"\n\n    def __init__(self, html, method=None, output_method=None):\n\n        self._method = method or self.default_parser_method\n        self._output_method = output_method or self.default_output_method\n\n        if self._output_method == 'xml':\n            self._html = html.replace('\\r\\n', '\\n')\n        else:\n            self._html = html\n\n        self._tree = None\n\n    @property\n    def html(self):\n        return self._html\n\n    @property\n    def tree(self):\n        if self._tree is None:\n            html_data = self._html.strip()\n            if self._method == 'xml':\n                parser = etree.XMLParser(ns_clean=False, resolve_entities=False)\n                self._tree = etree.fromstring(html_data, parser)\n            elif self._method == 'html5':\n                import html5lib\n                parsed = html5lib.parse(html_data, treebuilder='lxml', namespaceHTMLElements=False)\n                self._tree = parsed.getroot()\n            else:\n                parser = etree.HTMLParser()\n                self._tree = etree.fromstring(html_data, parser)\n        return self._tree\n\n    def to_string(self, encoding='utf-8', **kwargs):\n        if self.tree is None:\n            return \"\"\n        method = self._output_method\n        out = etree.tostring(self.tree, encoding=encoding, method=method, **kwargs).decode(encoding)\n        if method == 'xml':\n            out = self._cdata_regex.sub(\n                lambda m: '/*<![CDATA[*/%s/*]]>*/' % m.group(1),\n                out\n            )\n            # Remove empty \"<title/>\" which breaks html rendering (Fixes #43)\n            out = self._xml_title_regex.sub('', out)\n        return out\n\n    def apply_to_images(self, func, images=True, backgrounds=True, styles_uri=True):\n\n        def _apply_to_style_uri(style_text, func):\n            dirty = False\n            parser = CSSParser().parseStyle(style_text)\n            for prop in parser.getProperties(all=True):\n                for value in prop.propertyValue:\n                    if value.type == 'URI':\n                        old_uri = value.uri\n                        new_uri = func(old_uri, element=value)\n                        if new_uri != old_uri:\n                            dirty = True\n                            value.uri = new_uri\n            if dirty:\n                css_text = parser.cssText\n                return css_text.decode('utf-8') if isinstance(css_text, bytes) else css_text\n            else:\n                return style_text\n\n        if images:\n            # Apply to images from IMG tag\n            for img in self.tree.xpath(\".//img\"):\n                if 'src' in img.attrib:\n                    img.attrib['src'] = func(img.attrib['src'], element=img)\n\n        if backgrounds:\n            # Apply to images from <tag background=\"X\">\n            for item in self.tree.xpath(\"//@background\"):\n                tag = item.getparent()\n                tag.attrib['background'] = func(tag.attrib['background'], element=tag)\n\n        if styles_uri:\n            # Apply to style uri\n            for item in self.tree.xpath(\"//@style\"):\n                tag = item.getparent()\n                tag.attrib['style'] = _apply_to_style_uri(tag.attrib['style'], func=func)\n\n    def apply_to_links(self, func):\n        # Apply to images from IMG tag\n        for a in self.tree.xpath(\".//a\"):\n            if 'href' in a.attrib:\n                a.attrib['href'] = func(a.attrib['href'], element=a)\n\n    def add_content_type_meta(self, content_type=\"text/html\", charset=\"utf-8\", element_cls=etree.Element):\n\n        def _get_content_type_meta(head):\n            content_type_meta = None\n            for meta in head.find('meta') or []:\n                http_equiv = meta.get('http-equiv', None)\n                if http_equiv and (http_equiv.lower() == 'content_type'):\n                    content_type_meta = meta\n                    break\n            if content_type_meta is None:\n                content_type_meta = element_cls('meta')\n                head.append(content_type_meta)\n            return content_type_meta\n\n        head = self.tree.find('head')\n        if head is None:\n            # After Premailer.transform there are always HEAD tag\n            logging.warning('HEAD not found. This should not happen. Skip.')\n            return\n\n        meta = _get_content_type_meta(head)\n        meta.set('content', '%s; charset=%s' % (content_type, charset))\n        meta.set('http-equiv', \"Content-Type\")\n\n    def save(self, **kwargs):\n        self._html = self.to_string(**kwargs)\n\n\nclass BaseTransformer(HTMLParser):\n\n    UNSAFE_TAGS = ['script', 'object', 'iframe', 'frame', 'base', 'meta', 'link', 'style']\n\n    attachment_store_cls = MemoryFileStore\n    attachment_file_cls = LazyHTTPFile\n    html_attribute_name = 'data-emails'\n\n    def __init__(self, html, local_loader=None,\n                 attachment_store=None,\n                 requests_params=None, method=None, base_url=None):\n\n        HTMLParser.__init__(self, html=html, method=method)\n\n        self.attachment_store = attachment_store if attachment_store is not None else self.attachment_store_cls()\n        self.local_loader = local_loader\n        if base_url and not base_url.endswith('/'):\n            base_url = base_url + '/'\n        self.base_url = base_url\n        self.requests_params = requests_params\n\n        self._premailer = None\n\n    def get_absolute_url(self, url):\n\n        if not self.base_url:\n            return url\n\n        if url.startswith('//'):\n            if 'https://' in self.base_url:\n                url = 'https:' + url\n            else:\n                url = 'http:' + url\n            return url\n\n        if not (url.startswith('http://') or url.startswith('https://')):\n            url = urlparse.urljoin(self.base_url, posixpath.normpath(url))\n\n        return url\n\n    def attribute_value(self, el):\n        return el is not None \\\n               and hasattr(el, 'attrib') \\\n               and el.attrib.get(self.html_attribute_name) \\\n               or None\n\n    _attribute_value = attribute_value  # deprecated\n\n    def _default_attachment_check(self, el, hints):\n        if hints['attrib'] == 'ignore':\n            return False\n        else:\n            return True\n\n    def _load_attachment_func(self, uri, element=None, callback=None, **kw):\n\n        #\n        # Load uri from remote url or from local_store\n        # Return local uri\n        #\n\n        if uri[:5].lower() == 'data:':\n            return uri\n\n        if callback is None:\n            # Default callback: skip images with data-emails=\"ignore\" attribute\n            callback = lambda _, hints: hints['attrib'] != 'ignore'\n\n        attribute_value = self.attribute_value(element) or ''\n\n        # If callback returns False, skip attachment loading\n        if not callback(element, hints={'attrib': attribute_value}):\n            return uri\n\n        attachment = self.attachment_store.by_uri(uri)\n        if attachment is None:\n            attachment = self.attachment_file_cls(\n                uri=uri,\n                absolute_url=self.get_absolute_url(uri),\n                local_loader=self.local_loader,\n                content_disposition='inline' if 'inline' in attribute_value else None,\n                requests_args=self.requests_params)\n            self.attachment_store.add(attachment)\n        return attachment.filename\n\n    def get_premailer(self, **kw):\n        kw.setdefault('attribute_name', self.html_attribute_name)\n        kw.setdefault('method', self._method)\n        kw.setdefault('base_url', self.base_url)\n        kw.setdefault('local_loader', self.local_loader)\n        return LocalPremailer(html=self.tree, **kw)\n\n    @property\n    def premailer(self):\n        if self._premailer is None:\n            self._premailer = self.get_premailer()\n        return self._premailer\n\n    def remove_unsafe_tags(self):\n        for tag in self.UNSAFE_TAGS:\n            for el in self.tree.xpath(\".//%s\" % tag):\n                parent = el.getparent()\n                if parent is not None:\n                    parent.remove(el)\n        return self\n\n    def load_and_transform(self,\n                           css_inline=True,\n                           remove_unsafe_tags=True,\n                           make_links_absolute=True,\n                           set_content_type_meta=True,\n                           update_stylesheet=False,\n                           load_images=True,\n                           images_inline=False,\n                           **kw):\n\n        if not make_links_absolute:\n            # Now we use Premailer that always makes links absolute\n            warnings.warn(\"make_links_absolute=False is deprecated.\", DeprecationWarning)\n\n        if update_stylesheet:\n            # Premailer has no such feature.\n            warnings.warn(\"update_stylesheet=True is deprecated.\", DeprecationWarning)\n\n        # 1. Premailer make some transformations on self.root tree:\n        #  - load external css and make css inline\n        #  - make absolute href and src if base_url is set\n        if css_inline:\n            self.get_premailer(**kw).transform()\n\n        # 2. Load linked images and transform links\n        # If load_images is a function, use if as callback\n        if load_images:\n            if callable(load_images):\n                func = functools.partial(self._load_attachment_func, callback=load_images)\n            else:\n                func = self._load_attachment_func\n            self.apply_to_images(func)\n\n        # 3. Remove unsafe tags is requested\n        if remove_unsafe_tags:\n            self.remove_unsafe_tags()\n\n        # 4. Set <meta> content-type\n        if set_content_type_meta:\n            # TODO: may be remove this ?\n            self.add_content_type_meta()\n\n        # 5. Make images inline\n        if load_images and images_inline:\n            self.make_all_images_inline()\n\n        return self\n\n    def make_all_images_inline(self):\n        for a in self.attachment_store:\n            a.is_inline = True\n        self.synchronize_inline_images()\n        return self\n\n    def synchronize_inline_images(self, inline_names=None, non_inline_names=None):\n        \"\"\"\n        Set img src in html for images, marked as \"inline\" in attachments_store\n        \"\"\"\n\n        if inline_names is None or non_inline_names is None:\n\n            inline_names = {}\n            non_inline_names = {}\n\n            for a in self.attachment_store:\n                if a.is_inline:\n                    inline_names[a.filename] = a.content_id\n                else:\n                    non_inline_names[a.content_id] = a.filename\n\n        def _src_update_func(src, **kw):\n            if src.startswith('cid:'):\n                content_id = src[4:]\n                if content_id in non_inline_names:\n                    return non_inline_names[content_id]\n            else:\n                if src in inline_names:\n                    return 'cid:'+inline_names[src]\n            return src\n\n        self.apply_to_images(_src_update_func)\n\n        return self\n\n\nclass Transformer(BaseTransformer):\n    pass\n\n\nclass MessageTransformer(BaseTransformer):\n\n    def __init__(self, message, **kw):\n        self.message = message\n\n        html = message._html\n        if isinstance(html, BaseTemplate):\n            html = html.template_text\n\n        params = {'html': html, 'attachment_store': message.attachments}\n        params.update(kw)\n        BaseTransformer.__init__(self, **params)\n\n    def save(self):\n        m = self.message\n        if isinstance(m._html, BaseTemplate):\n            m._html.set_template_text(self.to_string())\n        else:\n            m._html = self.to_string()\n"
  },
  {
    "path": "emails/utils.py",
    "content": "from __future__ import annotations\n\nimport os\nimport socket\nfrom time import mktime\nfrom datetime import datetime\nfrom random import randrange\nfrom functools import wraps\nfrom io import StringIO, BytesIO\nfrom collections.abc import Callable\nfrom typing import Any, TypeVar, cast\n\nimport email.charset\nfrom email import generator\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header, decode_header as decode_header_\nfrom email.utils import parseaddr, formatdate\nfrom email.utils import escapesre, specialsre  # type: ignore[attr-defined]  # private but stable\n\nfrom . import USER_AGENT\nfrom .exc import HTTPLoaderError\n\nF = TypeVar('F', bound=Callable[..., Any])\n\n\ndef formataddr(pair: tuple[str | None, str]) -> str:\n    \"\"\"\n    Takes a 2-tuple of the form (realname, email_address) and returns RFC2822-like string.\n    Does not encode non-ascii realname (unlike stdlib email.utils.formataddr).\n    \"\"\"\n    name, address = pair\n    if name:\n        quotes = ''\n        if specialsre.search(name):\n            quotes = '\"'\n        name = escapesre.sub(r'\\\\\\g<0>', name)\n        return '%s%s%s <%s>' % (quotes, name, quotes, address)\n    return address\n\n\n_charsets_loaded = False\n\nCHARSETS_FIX = [\n    ['windows-1251', 'QP', 'QP'],\n    # koi8 should by send as Quoted Printable because of bad SpamAssassin reaction on base64 (2008)\n    ['koi8-r', 'QP', 'QP'],\n    ['utf-8', 'BASE64', 'BASE64']\n]\n\n\ndef load_email_charsets() -> None:\n    global _charsets_loaded\n    if not _charsets_loaded:\n        for (charset, header_enc, body_enc) in CHARSETS_FIX:\n            email.charset.add_charset(charset,\n                                      getattr(email.charset, header_enc),\n                                      getattr(email.charset, body_enc),\n                                      charset)\n\n\nclass cached_property:\n    \"\"\"\n    A property that is only computed once per instance and then replaces itself\n    with an ordinary attribute. Deleting the attribute resets the property.\n    Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76\n    \"\"\"  # noqa\n\n    def __init__(self, func: Callable[..., Any]) -> None:\n        self.__doc__ = getattr(func, '__doc__')\n        self.func = func\n\n    def __get__(self, obj: Any, cls: type | None = None) -> Any:\n        if obj is None:\n            return self\n        value = obj.__dict__[self.func.__name__] = self.func(obj)\n        return value\n\n\n# Django's CachedDnsName:\n# Cached the hostname, but do it lazily: socket.getfqdn() can take a couple of\n# seconds, which slows down the restart of the server.\nclass CachedDnsName:\n    def __str__(self) -> str:\n        return self.get_fqdn()\n\n    def get_fqdn(self) -> str:\n        if not hasattr(self, '_fqdn'):\n            self._fqdn = socket.getfqdn()\n        return self._fqdn\n\n\nDNS_NAME = CachedDnsName()\n\n\ndef decode_header(value: str | bytes, default: str = \"utf-8\", errors: str = 'strict') -> str:\n    \"\"\"Decode the specified header value\"\"\"\n    if isinstance(value, bytes):\n        value = value.decode(default, errors)\n    parts: list[str] = []\n    for text, charset in decode_header_(value):\n        if isinstance(text, bytes):\n            parts.append(text.decode(charset or default, errors))\n        else:\n            parts.append(text)\n    return \"\".join(parts)\n\n\nclass MessageID:\n    \"\"\"Returns a string suitable for RFC 2822 compliant Message-ID, e.g:\n    <20020201195627.33539.96671@nightshade.la.mastaler.com>\n    Optional idstring if given is a string used to strengthen the\n    uniqueness of the message id.\n    Based on django.core.mail.message.make_msgid\n    \"\"\"\n\n    def __init__(self, domain: str | None = None, idstring: str | int | None = None) -> None:\n        self.domain = str(domain or DNS_NAME)\n        try:\n            pid = os.getpid()\n        except AttributeError:\n            # No getpid() in Jython.\n            pid = 1\n        self.idstring = \".\".join([str(idstring or randrange(10000)), str(pid)])\n\n    def __call__(self) -> str:\n        r = \".\".join([datetime.now().strftime(\"%Y%m%d%H%M%S.%f\"),\n                      str(randrange(100000)),\n                      self.idstring])\n        return \"\".join(['<', r, '@', self.domain, '>'])\n\n\n# Type alias for address pairs used throughout the library\nAddressPair = tuple[str | None, str | None]\n\n\ndef parse_name_and_email_list(elements: str | tuple[str | None, str] | list[Any] | None,\n                              encoding: str = 'utf-8') -> list[AddressPair]:\n    \"\"\"\n    Parse a list of address-like elements, i.e.:\n     * \"name <email>\"\n     * \"email\"\n     * (name, email)\n\n    :param elements: one element or list of elements\n    :param encoding: element encoding, if bytes\n    :return: list of pairs (name, email)\n    \"\"\"\n    if not elements:\n        return []\n\n    if isinstance(elements, str):\n        return [parse_name_and_email(elements, encoding), ]\n\n    if not isinstance(elements, (list, tuple)):\n        raise TypeError(\"Can not parse_name_and_email_list from %s\" % elements.__repr__())\n\n    if len(elements) == 2:\n        # Oops, it may be pair (name, email) or pair of emails [email1, email2]\n        # Let's do some guesses\n        if isinstance(elements, tuple):\n            n, e = elements\n            if isinstance(e, str) and (not n or isinstance(n, str)):\n                # It is probably a pair (name, email)\n                return [parse_name_and_email(elements, encoding), ]\n\n    return [parse_name_and_email(x, encoding) for x in elements]\n\n\ndef parse_name_and_email(obj: str | tuple[str | None, str] | list[str],\n                         encoding: str = 'utf-8') -> AddressPair:\n    # In:  'john@smith.me' or  '\"John Smith\" <john@smith.me>' or ('John Smith', 'john@smith.me')\n    # Out: ('John Smith', 'john@smith.me')\n\n    if isinstance(obj, (list, tuple)):\n        if len(obj) == 2:\n            name, email = obj\n        else:\n            raise ValueError(\"Can not parse_name_and_email from %s\" % obj)\n    elif isinstance(obj, str):\n        name, email = parseaddr(obj)\n    else:\n        raise ValueError(\"Can not parse_name_and_email from %s\" % obj)\n\n    return name or None, email or None\n\n\ndef sanitize_email(addr: str, encoding: str = 'ascii', parse: bool = False) -> str:\n    if parse:\n        _, addr = parseaddr(addr)\n    try:\n        addr.encode('ascii')\n    except UnicodeEncodeError:  # IDN\n        if '@' in addr:\n            localpart, domain = addr.split('@', 1)\n            localpart = str(Header(localpart, encoding))\n            domain = domain.encode('idna').decode('ascii')\n            addr = '@'.join([localpart, domain])\n        else:\n            addr = Header(addr, encoding).encode()\n    return addr\n\n\ndef sanitize_address(addr: str | tuple[str, str], encoding: str = 'ascii') -> str:\n    if isinstance(addr, str):\n        addr = parseaddr(addr)\n    nm, addr = addr\n    # This try-except clause is needed on Python 3 < 3.2.4\n    # http://bugs.python.org/issue14291\n    try:\n        nm = Header(nm, encoding).encode()\n    except UnicodeEncodeError:\n        nm = Header(nm, 'utf-8').encode()\n    return formataddr((nm, sanitize_email(addr, encoding=encoding, parse=False)))\n\n\nclass MIMEMixin:\n    def as_string(self, unixfrom: bool = False, linesep: str = '\\n') -> str:\n        \"\"\"Return the entire formatted message as a string.\n        Optional `unixfrom' when True, means include the Unix From_ envelope\n        header.\n        This overrides the default as_string() implementation to not mangle\n        lines that begin with 'From '. See bug #13433 for details.\n        \"\"\"\n        fp = StringIO()\n        g = generator.Generator(fp, mangle_from_=False)\n        g.flatten(self, unixfrom=unixfrom, linesep=linesep)\n\n        return fp.getvalue()\n\n    def as_bytes(self, unixfrom: bool = False, linesep: str = '\\n') -> bytes:\n            \"\"\"Return the entire formatted message as bytes.\n            Optional `unixfrom' when True, means include the Unix From_ envelope\n            header.\n            This overrides the default as_bytes() implementation to not mangle\n            lines that begin with 'From '. See bug #13433 for details.\n            \"\"\"\n            fp = BytesIO()\n            g = generator.BytesGenerator(fp, mangle_from_=False)\n            g.flatten(self, unixfrom=unixfrom, linesep=linesep)\n            return fp.getvalue()\n\n\nclass SafeMIMEText(MIMEMixin, MIMEText):  # type: ignore[misc]  # intentional override\n    def __init__(self, text: str, subtype: str, charset: str) -> None:\n        self.encoding = charset\n        MIMEText.__init__(self, text, subtype, charset)\n\n\nclass SafeMIMEMultipart(MIMEMixin, MIMEMultipart):  # type: ignore[misc]  # intentional override\n    def __init__(self, _subtype: str = 'mixed', boundary: str | None = None,\n                 _subparts: list[Any] | None = None,\n                 encoding: str | None = None, **_params: Any) -> None:\n        self.encoding = encoding\n        MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)\n\n\nDEFAULT_REQUESTS_PARAMS: dict[str, Any] = dict(allow_redirects=True,\n                             verify=False, timeout=10,\n                             headers={'User-Agent': USER_AGENT})\n\n\ndef fetch_url(url: str, valid_http_codes: tuple[int, ...] = (200, ),\n              requests_args: dict[str, Any] | None = None) -> Any:\n    import requests\n    args = {}\n    args.update(DEFAULT_REQUESTS_PARAMS)\n    args.update(requests_args or {})\n    r = requests.get(url, **args)\n    if valid_http_codes and (r.status_code not in valid_http_codes):\n        raise HTTPLoaderError('Error loading url: %s. HTTP status: %s' % (url, r.status_code))\n    return r\n\n\ndef encode_header(value: str | Any, charset: str = 'utf-8') -> str | Any:\n    if isinstance(value, str):\n        value = value.rstrip()\n        _r = Header(value, charset)\n        return str(_r)\n    else:\n        return value\n\n\ndef renderable(f: F) -> F:\n    @wraps(f)\n    def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:\n        r = f(self, *args, **kwargs)\n        render = getattr(r, 'render', None)\n        if render:\n            d = render(**(self.render_data or {}))\n            return d\n        else:\n            return r\n\n    return cast(F, wrapper)\n\n\ndef format_date_header(v: datetime | float | None, localtime: bool = True) -> str:\n    if isinstance(v, datetime):\n        return formatdate(mktime(v.timetuple()), localtime)\n    elif isinstance(v, float):\n        # probably timestamp\n        return formatdate(v, localtime)\n    elif v is None:\n        return formatdate(None, localtime)\n    else:\n        return v\n"
  },
  {
    "path": "release.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# --- Check for uncommitted changes ---\nif ! git diff --quiet || ! git diff --cached --quiet; then\n    echo \"Error: there are uncommitted changes. Commit or stash them first.\"\n    exit 1\nfi\n\n# --- Read current version ---\nVERSION_FILE=\"emails/__init__.py\"\nCHANGELOG=\"CHANGELOG.md\"\n\ncurrent=$(sed -n \"s/^__version__ = '\\([^']*\\)'/\\1/p\" \"$VERSION_FILE\")\nif [[ -z \"$current\" ]]; then\n    echo \"Error: could not read version from $VERSION_FILE\"\n    exit 1\nfi\n\nIFS='.' read -r major minor patch <<< \"$current\"\npatch=${patch:-0}\nminor=${minor:-0}\n\nv_patch=\"$major.$minor.$((patch + 1))\"\nv_minor=\"$major.$((minor + 1)).0\"\nv_major=\"$((major + 1)).0.0\"\n\necho \"Current version: $current\"\necho \"\"\necho \"  1) patch  -> $v_patch\"\necho \"  2) minor  -> $v_minor\"\necho \"  3) major  -> $v_major\"\necho \"\"\nread -rp \"Choose [1/2/3]: \" choice\n\ncase \"$choice\" in\n    1) new_version=\"$v_patch\" ;;\n    2) new_version=\"$v_minor\" ;;\n    3) new_version=\"$v_major\" ;;\n    *) echo \"Invalid choice\"; exit 1 ;;\nesac\n\necho \"\"\necho \"Releasing $current -> $new_version\"\necho \"\"\n\n# --- Update version in emails/__init__.py ---\nsed -i '' \"s/^__version__ = '${current}'/__version__ = '${new_version}'/\" \"$VERSION_FILE\"\n\n# --- Update CHANGELOG: add date to the release heading ---\ntoday=$(date +%Y-%m-%d)\nsed -i '' \"s/^## ${new_version}$/## ${new_version} — ${today}/\" \"$CHANGELOG\"\n# Also handle case where heading doesn't have the version yet (uses Unreleased)\nsed -i '' \"s/^## Unreleased$/## ${new_version} — ${today}/\" \"$CHANGELOG\"\n\n# --- Commit and tag ---\ngit add \"$VERSION_FILE\" \"$CHANGELOG\"\ngit commit -m \"Release v${new_version}\"\ngit tag \"v${new_version}\"\n\necho \"\"\necho \"Done. Created commit and tag v${new_version}.\"\necho \"Run 'git push && git push --tags' to publish.\"\n"
  },
  {
    "path": "requirements/base.txt",
    "content": "cssutils\nlxml\nchardet\npython-dateutil\nrequests\npremailer>=2.8.3\npuremagic\ndkimpy\n"
  },
  {
    "path": "requirements/tests-base.txt",
    "content": "jinja2\nmako\nspeaklater\npytest\npytest-cov\npytest-asyncio\nhtml5lib\naiosmtplib\ncryptography\n"
  },
  {
    "path": "requirements/tests-django.txt",
    "content": "--requirement=base.txt\n--requirement=tests-base.txt\n"
  },
  {
    "path": "requirements/tests.txt",
    "content": "--requirement=base.txt\n--requirement=tests-base.txt\n\ndjango"
  },
  {
    "path": "scripts/make_rfc822.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\n\nSimple utility that imports html from url ang print generated rfc822 message to console.\n\nExample usage:\n\n    $ python make_rfc822.py \\\n            --url=http://lavr.github.io/python-emails/tests/campaignmonitor-samples/sample-template/template-widgets.html \\\n            --inline-images \\\n            --subject=\"Some subject\" \\\n            --from-name=\"Sergey Lavrinenko\" \\\n            --from-email=s@lavr.me \\\n            --message-id-domain=localhost \\\n            --add-header=\"X-Test-Header: Test\" \\\n            --add-header-imported-from \\\n            --send-test-email-to=sergei-nko@mail.ru \\\n            --smtp-host=mxs.mail.ru \\\n            --smtp-port=25\n\nCopyright 2013  Sergey Lavrinenko <s@lavr.me>\n\n\"\"\"\n\nimport sys\nimport logging\nimport json\nimport argparse\n\nimport emails\nimport emails.loader\nfrom emails.template import JinjaTemplate as T\n\n\nclass MakeRFC822:\n    def __init__(self, options):\n        self.options = options\n\n    def _headers_from_command_line(self):\n        \"\"\"\n        --add-header \"X-Source: AAA\"\n        \"\"\"\n        r = {}\n        if self.options.add_headers:\n            for s in self.options.add_headers:\n                (k, v) = s.split(':', 1)\n                r[k] = v\n\n        if self.options.add_header_imported_from:\n            r['X-Imported-From-URL'] = self.options.url\n\n        return r\n\n    def _get_message(self):\n\n        options = self.options\n\n        if options.message_id_domain:\n            message_id = emails.MessageID(domain=options.message_id_domain)\n        else:\n            message_id = None\n\n        args = dict(images_inline=options.inline_images,\n                    message_params=dict(headers=self._headers_from_command_line(),\n                                        mail_from=(options.from_name, options.from_email),\n                                        subject=T(options.subject),\n                                        message_id=message_id),\n                    template_cls=T)\n        if options.url:\n            message = emails.loader.from_url(url=options.url, **args)\n        elif options.from_directory:\n            message = emails.loader.from_directory(options.from_directory, **args)\n        elif options.from_file:\n            message = emails.loader.from_file(options.from_file, **args)\n        elif options.from_zipfile:\n            message = emails.loader.from_zip(options.from_zipfile, **args)\n        else:\n            logging.error('No message source specified.')\n            sys.exit(1)\n\n        return message\n\n    def _send_test_email(self, message):\n\n        options = self.options\n\n        if options.send_test_email_to:\n            logging.debug(\"options.send_test_email_to YES\")\n\n            smtp_params = {}\n            for k in ('host', 'port', 'ssl', 'user', 'password', 'debug'):\n                smtp_params[k] = getattr(options, 'smtp_%s' % k, None)\n\n            for mail_to in options.send_test_email_to.split(','):\n                r = message.send(to=mail_to, smtp=smtp_params)\n                logging.debug(\"mail_to=%s result=%s error=%s\", mail_to, r, r.error)\n                if r.error:\n                    raise r.error\n\n    def _start_batch(self):\n\n        fn = self.options.batch\n        if not fn:\n            return None\n\n        if fn == '-':\n            f = sys.stdin\n        else:\n            f = open(fn, 'rb')\n\n        def wrapper():\n            for l in f.readlines():\n                l = l.strip()\n                if not l:\n                    continue\n                try:\n                    # Try to parse line as json\n                    yield json.loads(l)\n                except ValueError:\n                    # If it is not json, we expect one word with '@' sign\n                    assert len(l.split()) == 1\n                    login, domain = l.split('@')  # ensure there is something email-like\n                    yield {'to': l}\n\n        return wrapper()\n\n    def _generate_batch(self, batch, message):\n        n = 0\n        for values in batch:\n            message.set_mail_to(values['to'])\n            message.render(**values.get('data', {}))\n            s = message.as_string()\n            n += 1\n            logging.debug('Render email to %s', '%s.eml' % n)\n            open('%s.eml' % n, 'wb').write(s)\n\n    def main(self):\n\n        message = self._get_message()\n\n        if self.options.batch:\n            batch = self._start_batch()\n            self._generate_batch(batch, message)\n        else:\n            if self.options.output_format == 'eml':\n                print(message.as_string())\n            elif self.options.output_format == 'html':\n                print(message.html_body)\n\n        self._send_test_email(message)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description='Imports html from url ang generate rfc822 message.')\n\n    parser.add_argument(\"-u\", \"--url\", metavar=\"URL\", dest=\"url\", action=\"store\", default=None)\n    parser.add_argument(\"--source-directory\", dest=\"from_directory\", action=\"store\", default=None)\n    parser.add_argument(\"--source-file\", dest=\"from_file\", action=\"store\", default=None)\n    parser.add_argument(\"--source-zipfile\", dest=\"from_zipfile\", action=\"store\", default=None)\n\n    parser.add_argument(\"-f\", \"--from-email\", metavar=\"EMAIL\", dest=\"from_email\", default=None, required=True)\n    parser.add_argument(\"-n\", \"--from-name\", metavar=\"NAME\", dest=\"from_name\", default=None, required=True)\n    parser.add_argument(\"-s\", \"--subject\", metavar=\"SUBJECT\", dest=\"subject\", default=None, required=True)\n    parser.add_argument(\"--message-id-domain\", dest=\"message_id_domain\", default=None, required=True)\n\n    parser.add_argument(\"--add-header\", dest=\"add_headers\", action='append', default=None, required=False)\n    parser.add_argument(\"--add-header-imported-from\", dest=\"add_header_imported_from\", default=False,\n                        action=\"store_true\")\n\n    parser.add_argument(\"--inline-images\", action=\"store_true\", dest=\"inline_images\", default=False)\n\n    parser.add_argument(\"--output-format\", dest=\"output_format\", default='eml', choices=['eml', ])\n    parser.add_argument(\"--log-level\", dest=\"log_level\", default=\"debug\")\n\n    parser.add_argument(\"--send-test-email-to\", dest=\"send_test_email_to\", default=None)\n    parser.add_argument(\"--smtp-host\", dest=\"smtp_host\", default=\"localhost\")\n    parser.add_argument(\"--smtp-port\", dest=\"smtp_port\", default=\"25\")\n    parser.add_argument(\"--smtp-ssl\", dest=\"smtp_ssl\", action=\"store_true\")\n    parser.add_argument(\"--smtp-user\", dest=\"smtp_user\", default=None)\n    parser.add_argument(\"--smtp-password\", dest=\"smtp_password\", default=None)\n    parser.add_argument(\"--smtp-debug\", dest=\"smtp_debug\", action=\"store_true\")\n\n    parser.add_argument(\"--batch\", dest=\"batch\", default=None)\n    parser.add_argument(\"--batch-start\", dest=\"batch_start\", default=None)\n    parser.add_argument(\"--batch-limit\", dest=\"batch_limit\", default=None)\n\n    options = parser.parse_args()\n\n    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()))\n\n    MakeRFC822(options=options).main()\n"
  },
  {
    "path": "setup.cfg",
    "content": "[mypy]\npython_version = 3.10\nignore_missing_imports = true\nwarn_return_any = true\nwarn_unused_ignores = true\nexclude = emails/testsuite/\n\n# Mixin classes access attributes defined in BaseMessage via MRO;\n# mypy checks each mixin in isolation and reports false attr-defined errors.\n# Also suppress arg-type/misc/union-attr/return-value/no-any-return which\n# stem from the same mixin pattern (self is typed as the mixin, not Message).\n[mypy-emails.message]\ndisable_error_code = attr-defined, arg-type, misc, union-attr, return-value, no-any-return, assignment\n\n# Uses private smtplib attrs (_have_ssl), conditional class definitions,\n# and overrides smtplib.SMTP.sendmail with incompatible return type\n[mypy-emails.backend.smtp.client]\ndisable_error_code = attr-defined, no-redef, override, no-any-return, assignment\n\n# aiosmtplib is an optional dependency (pip install emails[async])\n[mypy-aiosmtplib]\nignore_missing_imports = true\n\n[mypy-aiosmtplib.*]\nignore_missing_imports = true\n\n# Optional dependency stubs\n[mypy-requests.*]\nignore_missing_imports = true\n\n# Optional dependency, not always installed\n[mypy-emails.template.*]\nignore_errors = true\n\n# Django integration, not always installed\n[mypy-emails.django]\nignore_errors = true\n\n[tool:pytest]\nnorecursedirs = .* {arch} *.egg *.egg-info dist build requirements\nmarkers =\n    e2e: tests that require a running SMTP server\n    django: tests that require Django\n\n[coverage:run]\nomit = emails/testsuite/*\n\n[coverage:report]\nomit = emails/testsuite/*\n"
  },
  {
    "path": "setup.py",
    "content": "\"\"\"Setup configuration for python-emails.\"\"\"\n\nimport codecs\nimport os\nimport re\nimport sys\n\n\ntry:\n    from setuptools import setup\nexcept ImportError:\n    from distutils.core import setup\n\nsettings = dict()\n\n# Publish Helper.\nif sys.argv[-1] == 'publish':\n    os.system('python setup.py sdist upload')\n    sys.exit()\n\nfrom setuptools import Command, setup\n\nclass run_audit(Command):\n    \"\"\"\n    By mitsuhiko's code:\n    Audits source code using PyFlakes for following issues:\n        - Names which are used but not defined or used before they are defined.\n        - Names which are redefined without having been used.\n    \"\"\"\n    description = \"Audit source code with PyFlakes\"\n    user_options = []\n\n    def initialize_options(self):\n        pass\n\n    def finalize_options(self):\n        pass\n\n    def run(self):\n        import os, sys\n        try:\n            import pyflakes.scripts.pyflakes as flakes\n        except ImportError:\n            print(\"Audit requires PyFlakes installed in your system.\")\n            sys.exit(-1)\n\n        warns = 0\n        # Define top-level directories\n        dirs = ('emails', 'scripts')\n        for dir in dirs:\n            for root, _, files in os.walk(dir):\n                for file in files:\n                    if file != '__init__.py' and file.endswith('.py') :\n                        warns += flakes.checkPath(os.path.join(root, file))\n        if warns > 0:\n            print(\"Audit finished with total %d warnings.\" % warns)\n        else:\n            print(\"No problems found in sourcecode.\")\n\ndef find_version(*file_paths):\n    version_file_path = os.path.join(os.path.dirname(__file__),\n                                     *file_paths)\n    version_file = codecs.open(version_file_path,\n                               encoding='utf-8').read()\n    version_match = re.search(r\"^__version__ = ['\\\"]([^'\\\"]*)['\\\"]\",\n                              version_file, re.M)\n    if version_match:\n        return version_match.group(1)\n    raise RuntimeError(\"Unable to find version string.\")\n\n\ndef read_file(filename):\n    file_path = os.path.join(os.path.dirname(__file__), filename)\n    with codecs.open(file_path, encoding='utf-8') as fh:\n        return fh.read()\n\n\nsettings.update(\n    name='emails',\n    version=find_version('emails/__init__.py'),\n    description='Modern python library for emails.',\n    long_description=read_file('README.rst'),\n    long_description_content_type='text/x-rst',\n    license='Apache License 2.0',\n    author='Sergey Lavrinenko',\n    author_email='s@lavr.me',\n    url='https://github.com/lavr/python-emails',\n    packages=['emails',\n              'emails.django',\n              'emails.loader',\n              'emails.store',\n              'emails.backend',\n              'emails.backend.smtp',\n              'emails.backend.inmemory',\n              'emails.template',\n             ],\n    package_data={'emails': ['py.typed']},\n    scripts=['scripts/make_rfc822.py'],\n    python_requires='>=3.10',\n    install_requires=['python-dateutil', 'puremagic', 'dkimpy'],\n    extras_require={\n        'html': ['cssutils', 'lxml', 'chardet', 'requests', 'premailer'],\n        'jinja': ['jinja2'],\n        'async': ['aiosmtplib'],\n    },\n    zip_safe=False,\n    classifiers=[\n        'Development Status :: 5 - Production/Stable',\n        'Intended Audience :: Developers',\n        'License :: OSI Approved :: Apache Software License',\n        'Natural Language :: English',\n        \"Operating System :: OS Independent\",\n        \"Programming Language :: Python\",\n        \"Programming Language :: Python :: 3\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3.11\",\n        \"Programming Language :: Python :: 3.12\",\n        \"Programming Language :: Python :: 3.13\",\n        \"Programming Language :: Python :: 3.14\",\n        \"Topic :: Communications\",\n        \"Topic :: Internet :: WWW/HTTP\",\n        \"Topic :: Other/Nonlisted Topic\",\n        \"Topic :: Software Development :: Libraries :: Python Modules\",\n    ],\n    cmdclass={'audit': run_audit}\n)\n\n\nsetup(**settings)\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py310, py311, py312, py313, py314, pypy, style\n\n[testenv]\npassenv = TEST_*,SMTP_TEST_*\ncommands = py.test --cov-report term --cov-report html --cov emails --cov-config=setup.cfg {posargs}\n\n[testenv:pypy]\ndeps =\n    -rrequirements/tests.txt\n\n[testenv:py310]\ndeps =\n    -rrequirements/tests.txt\n\n[testenv:py311]\ndeps =\n    -rrequirements/tests.txt\n\n[testenv:py312]\ndeps =\n    -rrequirements/tests.txt\n\n[testenv:py313]\ndeps =\n    -rrequirements/tests.txt\n\n[testenv:py314]\ndeps =\n    -rrequirements/tests.txt\n\n\n[testenv:django42]\nbasepython = python3.12\ndeps =\n    -rrequirements/tests-django.txt\n    Django>=4.2,<4.3\ncommands = py.test --cov-report term --cov-report html --cov emails --cov-config=setup.cfg -m django {posargs}\n\n[testenv:django52]\nbasepython = python3.12\ndeps =\n    -rrequirements/tests-django.txt\n    Django>=5.2,<5.3\ncommands = py.test --cov-report term --cov-report html --cov emails --cov-config=setup.cfg -m django {posargs}\n\n[testenv:django60]\nbasepython = python3.12\ndeps =\n    -rrequirements/tests-django.txt\n    Django>=6.0,<6.1\ncommands = py.test --cov-report term --cov-report html --cov emails --cov-config=setup.cfg -m django {posargs}\n\n[testenv:typecheck]\ndeps = mypy\ncommands = mypy emails/\n\n[testenv:style]\ndeps = pre-commit\nskip_install = true\ncommands = pre-commit run --all-files --show-diff-on-failure"
  }
]