├── .dockerignore ├── .env_sample ├── .flake8 ├── .github └── workflows │ ├── pr-lint.yml │ └── test-and-deploy.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── FIRST_TIMERS.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.rst ├── TROUBLESHOOTING.md ├── USAGE.md ├── VERSION.txt ├── changes.py ├── docker-compose.yml ├── examples └── example.py ├── run.sh ├── setup.py ├── smtpapi └── __init__.py ├── static └── img │ ├── github-fork.png │ └── github-sign-up.png ├── test ├── __init__.py ├── requirements.txt └── test_project.py ├── twilio_sendgrid_logo.png └── use_cases └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg 3 | *.egg-info 4 | dist 5 | eggs 6 | build 7 | sdist 8 | .Python 9 | bin/ 10 | include/ 11 | lib/ 12 | -------------------------------------------------------------------------------- /.env_sample: -------------------------------------------------------------------------------- 1 | export SENDGRID_API_KEY='' 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = build,venv,.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg 3 | -------------------------------------------------------------------------------- /.github/workflows/pr-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR 2 | on: 3 | pull_request_target: 4 | types: [ opened, edited, synchronize, reopened ] 5 | 6 | jobs: 7 | validate: 8 | name: Validate title 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: amannn/action-semantic-pull-request@v4 12 | with: 13 | types: chore docs fix feat test misc 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/test-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | on: 3 | push: 4 | branches: [ '*' ] 5 | tags: [ '*' ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Run automatically at 8AM PST Monday-Friday 10 | - cron: '0 15 * * 1-5' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 20 18 | strategy: 19 | matrix: 20 | python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10' ] 21 | steps: 22 | - name: Checkout sendgrid-python-smtpapi 23 | uses: actions/checkout@v2 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Build & Test 31 | run: make install test-install test 32 | 33 | deploy: 34 | name: Deploy 35 | if: success() && github.ref_type == 'tag' 36 | needs: [ test ] 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sendgrid-python-smtpapi 40 | uses: actions/checkout@v2 41 | 42 | - name: Set up Python 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: '3.10' 46 | 47 | - name: Install dependencies 48 | run: | 49 | python -m pip install --upgrade pip 50 | pip install build 51 | pip install wheel 52 | python setup.py sdist bdist_wheel 53 | 54 | - name: Create GitHub Release 55 | uses: sendgrid/dx-automator/actions/release@main 56 | with: 57 | footer: '**[pypi](https://pypi.org/project/smtpapi/${version})**' 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - name: Publish package to PyPI 62 | uses: pypa/gh-action-pypi-publish@release/v1 63 | with: 64 | user: __token__ 65 | password: ${{ secrets.PYPI_TOKEN }} 66 | 67 | - name: Submit metric to Datadog 68 | uses: sendgrid/dx-automator/actions/datadog-release-metric@main 69 | env: 70 | DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} 71 | 72 | notify-on-failure: 73 | name: Slack notify on failure 74 | if: failure() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') 75 | needs: [ test, deploy ] 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: rtCamp/action-slack-notify@v2 79 | env: 80 | SLACK_COLOR: failure 81 | SLACK_ICON_EMOJI: ':github:' 82 | SLACK_MESSAGE: ${{ format('Test *{0}*, Deploy *{1}*, {2}/{3}/actions/runs/{4}', needs.test.result, needs.deploy.result, github.server_url, github.repository, github.run_id) }} 83 | SLACK_TITLE: Action Failure - ${{ github.repository }} 84 | SLACK_USERNAME: GitHub Actions 85 | SLACK_MSG_AUTHOR: twilio-dx 86 | SLACK_FOOTER: Posted automatically using GitHub Actions 87 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 88 | MSG_MINIMAL: true 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | *.pyc 4 | .coverage 5 | .env 6 | .Python 7 | bin/ 8 | build 9 | dist 10 | eggs 11 | include/ 12 | lib/ 13 | sdist 14 | smtpapi/VERSION.txt 15 | venv/ 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All the notable changes to this project will be documented in this file. 3 | 4 | [2022-03-09] Version 0.4.12 5 | --------------------------- 6 | **Library - Chore** 7 | - [PR #112](https://github.com/sendgrid/smtpapi-python/pull/112): push Datadog Release Metric upon deploy success. Thanks to [@eshanholtz](https://github.com/eshanholtz)! 8 | 9 | 10 | [2022-02-09] Version 0.4.11 11 | --------------------------- 12 | **Library - Chore** 13 | - [PR #111](https://github.com/sendgrid/smtpapi-python/pull/111): upgrade supported language versions. Thanks to [@childish-sambino](https://github.com/childish-sambino)! 14 | - [PR #110](https://github.com/sendgrid/smtpapi-python/pull/110): add gh release to workflow. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! 15 | - [PR #109](https://github.com/sendgrid/smtpapi-python/pull/109): merge test and deploy gh action workflows. Thanks to [@Hunga1](https://github.com/Hunga1)! 16 | 17 | 18 | [2022-01-12] Version 0.4.10 19 | --------------------------- 20 | **Library - Chore** 21 | - [PR #108](https://github.com/sendgrid/smtpapi-python/pull/108): update license year. Thanks to [@JenniferMah](https://github.com/JenniferMah)! 22 | 23 | 24 | [2021-12-01] Version 0.4.9 25 | -------------------------- 26 | **Library - Chore** 27 | - [PR #107](https://github.com/sendgrid/smtpapi-python/pull/107): migrate to GitHub Actions. Thanks to [@JenniferMah](https://github.com/JenniferMah)! 28 | 29 | 30 | [2021-09-22] Version 0.4.8 31 | -------------------------- 32 | **Library - Chore** 33 | - [PR #106](https://github.com/sendgrid/smtpapi-python/pull/106): test against python v3.9. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! 34 | 35 | 36 | [2020-12-02] Version 0.4.7 37 | -------------------------- 38 | **Library - Chore** 39 | - [PR #63](https://github.com/sendgrid/smtpapi-python/pull/63): Add .codeclimate.yml and update test/__init__.py. Thanks to [@geomars](https://github.com/geomars)! 40 | 41 | 42 | [2020-09-28] Version 0.4.6 43 | -------------------------- 44 | **Library - Docs** 45 | - [PR #57](https://github.com/sendgrid/smtpapi-python/pull/57): Few typos and grammatical mistake corrected. Thanks to [@yudhik11](https://github.com/yudhik11)! 46 | - [PR #61](https://github.com/sendgrid/smtpapi-python/pull/61): Add pull request info to CONTRIBUTING and README. Thanks to [@shorrock](https://github.com/shorrock)! 47 | 48 | 49 | [2020-08-19] Version 0.4.5 50 | -------------------------- 51 | **Library - Docs** 52 | - [PR #72](https://github.com/sendgrid/smtpapi-python/pull/72): Fix .md with Grammar.ly. Thanks to [@ssiddhantsharma](https://github.com/ssiddhantsharma)! 53 | - [PR #70](https://github.com/sendgrid/smtpapi-python/pull/70): Correct *.md files using Grammarly. Thanks to [@myzeprog](https://github.com/myzeprog)! 54 | 55 | **Library - Chore** 56 | - [PR #105](https://github.com/sendgrid/smtpapi-python/pull/105): update GitHub branch references to use HEAD. Thanks to [@thinkingserious](https://github.com/thinkingserious)! 57 | 58 | 59 | [2020-07-08] Version 0.4.4 60 | -------------------------- 61 | **Library - Fix** 62 | - [PR #86](https://github.com/sendgrid/smtpapi-python/pull/86): Fix and add flake8 to CI. Thanks to [@hugovk](https://github.com/hugovk)! 63 | 64 | **Library - Docs** 65 | - [PR #99](https://github.com/sendgrid/smtpapi-python/pull/99): add use cases directory and README.md file for use cases. Thanks to [@mtroiani](https://github.com/mtroiani)! 66 | 67 | 68 | [2020-04-01] Version 0.4.3 69 | -------------------------- 70 | **Library - Chore** 71 | - [PR #104](https://github.com/sendgrid/smtpapi-python/pull/104): add Python 3.8 to Travis. Thanks to [@childish-sambino](https://github.com/childish-sambino)! 72 | 73 | 74 | [2020-02-19] Version 0.4.2 75 | -------------------------- 76 | **Library - Fix** 77 | - [PR #53](https://github.com/sendgrid/smtpapi-python/pull/53): "similar-code" issue in smtpapi/__init__.py. Thanks to [@andre8359](https://github.com/andre8359)! 78 | 79 | **Library - Docs** 80 | - [PR #88](https://github.com/sendgrid/smtpapi-python/pull/88): Fix typos and grammatical errors. Thanks to [@vinayak42](https://github.com/vinayak42)! 81 | 82 | 83 | [2020-01-24] Version 0.4.1 84 | -------------------------- 85 | **Library - Fix** 86 | - [PR #103](https://github.com/sendgrid/smtpapi-python/pull/103): travis update for autodeploy. Thanks to [@thinkingserious](https://github.com/thinkingserious)! 87 | 88 | 89 | [2020-01-24] Version 0.4.0 90 | -------------------------- 91 | **Library - Docs** 92 | - [PR #102](https://github.com/sendgrid/smtpapi-python/pull/102): baseline all the templated markdown docs. Thanks to [@childish-sambino](https://github.com/childish-sambino)! 93 | - [PR #77](https://github.com/sendgrid/smtpapi-python/pull/77): add first-timers.md with guide for newcomers. Thanks to [@daniloff200](https://github.com/daniloff200)! 94 | - [PR #79](https://github.com/sendgrid/smtpapi-python/pull/79): Update contribution to use Gitflow workflow. Thanks to [@anatolyyyyyy](https://github.com/anatolyyyyyy)! 95 | - [PR #93](https://github.com/sendgrid/smtpapi-python/pull/93): Added Announcement. Thanks to [@krischoi07](https://github.com/krischoi07)! 96 | - [PR #34](https://github.com/sendgrid/smtpapi-python/pull/34): Add USAGE.md. Thanks to [@geomars](https://github.com/geomars)! 97 | - [PR #27](https://github.com/sendgrid/smtpapi-python/pull/27): update contributing.md - fix typo. Thanks to [@pushkyn](https://github.com/pushkyn)! 98 | - [PR #26](https://github.com/sendgrid/smtpapi-python/pull/26): update README.md. Thanks to [@thepriefy](https://github.com/thepriefy)! 99 | - [PR #23](https://github.com/sendgrid/smtpapi-python/pull/23): ADD ISSUE_TEMPLATE. Thanks to [@prashant0598](https://github.com/prashant0598)! 100 | - [PR #21](https://github.com/sendgrid/smtpapi-python/pull/21): add table of contents in README.md. Thanks to [@thepriefy](https://github.com/thepriefy)! 101 | - [PR #19](https://github.com/sendgrid/smtpapi-python/pull/19): Update README. Thanks to [@micahduron](https://github.com/micahduron)! 102 | - [PR #12](https://github.com/sendgrid/smtpapi-python/pull/12): Update README.md. Thanks to [@ciceropablo](https://github.com/ciceropablo)! 103 | - [PR #20](https://github.com/sendgrid/smtpapi-python/pull/20): Demonstrate how to review the request body for troubleshooting. Thanks to [@mptap](https://github.com/mptap)! 104 | 105 | **Library - Feature** 106 | - [PR #35](https://github.com/sendgrid/smtpapi-python/pull/35): Add docker files and update README. Thanks to [@NdagiStanley](https://github.com/NdagiStanley)! 107 | - [PR #80](https://github.com/sendgrid/smtpapi-python/pull/80): Adding CHANGELOG auto generation script. Thanks to [@freyamade](https://github.com/freyamade)! 108 | 109 | **Library - Chore** 110 | - [PR #37](https://github.com/sendgrid/smtpapi-python/pull/37): Created Code Climate YML file. Thanks to [@prashuchaudhary](https://github.com/prashuchaudhary)! 111 | - [PR #41](https://github.com/sendgrid/smtpapi-python/pull/41): update LICENSE - fix year. Thanks to [@pushkyn](https://github.com/pushkyn)! 112 | - [PR #65](https://github.com/sendgrid/smtpapi-python/pull/65): Fix "Complexity" issue in examples/example.py. Thanks to [@thepriefy](https://github.com/thepriefy)! 113 | - [PR #66](https://github.com/sendgrid/smtpapi-python/pull/66): Update LICENSE.txt. Thanks to [@nocategory](https://github.com/nocategory)! 114 | - [PR #87](https://github.com/sendgrid/smtpapi-python/pull/87): PEP8 fix. Thanks to [@vkmrishad](https://github.com/vkmrishad)! 115 | - [PR #33](https://github.com/sendgrid/smtpapi-python/pull/33): Add .env_sample. Thanks to [@CorneliusIV](https://github.com/CorneliusIV)! 116 | - [PR #29](https://github.com/sendgrid/smtpapi-python/pull/29): Create PULL_REQUEST_TEMPLATE.md. Thanks to [@random-hacktoberfest-participant-2017](https://github.com/random-hacktoberfest-participant-2017)! 117 | - [PR #25](https://github.com/sendgrid/smtpapi-python/pull/25): Added CodeCov Badge. Thanks to [@gr8shivam](https://github.com/gr8shivam)! 118 | 119 | **Library - Test** 120 | - [PR #42](https://github.com/sendgrid/smtpapi-python/pull/42): Issue#40 unittest to check for specific repo files. Thanks to [@bertuss](https://github.com/bertuss)! 121 | - [PR #43](https://github.com/sendgrid/smtpapi-python/pull/43): added unittest to check for specific repo files. Thanks to [@riyasyash](https://github.com/riyasyash)! 122 | - [PR #44](https://github.com/sendgrid/smtpapi-python/pull/44): added test_project.py for testing files in dir. Thanks to [@garuna-m6](https://github.com/garuna-m6)! 123 | - [PR #45](https://github.com/sendgrid/smtpapi-python/pull/45): Add unit test for license year. Thanks to [@pushkyn](https://github.com/pushkyn)! 124 | - [PR #48](https://github.com/sendgrid/smtpapi-python/pull/48): Test added for checking license year. Thanks to [@parth-p](https://github.com/parth-p)! 125 | 126 | **Library - Fix** 127 | - [PR #52](https://github.com/sendgrid/smtpapi-python/pull/52): Update travis - add codecov. Thanks to [@pushkyn](https://github.com/pushkyn)! 128 | - [PR #75](https://github.com/sendgrid/smtpapi-python/pull/75): fixed Travis, converted README and version as file. Thanks to [@StrikerRUS](https://github.com/StrikerRUS)! 129 | 130 | 131 | [2015-10-01] Version 0.3.1 132 | --------------------------- 133 | 134 | ### Added 135 | - Added in support for `Decimal` objects in SMTP API JSON messages (via @jstol) 136 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hello! Thank you for choosing to help contribute to one of the Twilio SendGrid open source projects. There are many ways you can contribute and help is always welcome. We simply ask that you follow the following contribution policies. 2 | 3 | - [Improvements to the Codebase](#improvements-to-the-codebase) 4 | - [Understanding the Code Base](#understanding-the-codebase) 5 | - [Testing](#testing) 6 | - [Style Guidelines & Naming Conventions](#style-guidelines-and-naming-conventions) 7 | - [Creating a Pull Request](#creating-a-pull-request) 8 | - [Code Reviews](#code-reviews) 9 | 10 | 11 | ## Improvements to the Codebase 12 | 13 | We welcome direct contributions to the smtpapi-python code base. Thank you! 14 | 15 | Please note that we use the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) for Git to help keep project development organized and consistent. 16 | 17 | ### Development Environment ### 18 | 19 | #### Install and Run Locally #### 20 | 21 | ##### Prerequisites ##### 22 | 23 | - Python 2.6, 2.7, 3.4 and 3.5 24 | 25 | ##### Initial setup: ##### 26 | 27 | ```bash 28 | git clone https://github.com/sendgrid/smtpapi-python.git 29 | cd smtpapi-python 30 | ``` 31 | 32 | ##### Execute: ##### 33 | 34 | See the [examples folder](examples) to get started quickly. 35 | 36 | 37 | ## Understanding the Code Base 38 | 39 | **/examples** 40 | 41 | Working examples that demonstrate usage. 42 | 43 | **/test** 44 | 45 | Unit tests. 46 | 47 | **/smtpapi/__init__.py** 48 | 49 | Builds the SMTPAPI header. 50 | 51 | 52 | ## Testing 53 | 54 | All PRs require passing tests before the PR will be reviewed. 55 | 56 | All the test files are in the [`test`](test) directory. 57 | 58 | For the purposes of contributing to this repo, please update the [`__init__.py`](test/__init__.py) file with unit tests as you modify the code. 59 | 60 | ```bash 61 | python setup.py install 62 | python test/__init__.py 63 | ``` 64 | 65 | 66 | ## Style Guidelines & Naming Conventions 67 | 68 | Generally, we follow the style guidelines as suggested by the official language. However, we ask that you conform to the styles that already exist in the library. If you wish to deviate, please explain your reasoning. 69 | 70 | - [PEP8](https://www.python.org/dev/peps/pep-0008/) 71 | 72 | Please run your code through: 73 | 74 | - [pyflakes](https://pypi.python.org/pypi/pyflakes) 75 | - [pylint](https://www.pylint.org/) 76 | - [pep8](https://pypi.python.org/pypi/pep8) 77 | 78 | ## Creating a Pull Request 79 | 80 | 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your fork, 81 | and configure the remotes: 82 | 83 | ```bash 84 | # Clone your fork of the repo into the current directory 85 | git clone https://github.com/sendgrid/smtpapi-python 86 | # Navigate to the newly cloned directory 87 | cd smtpapi-python 88 | # Assign the original repo to a remote called "upstream" 89 | git remote add upstream https://github.com/sendgrid/smtpapi-python 90 | ``` 91 | 92 | 2. If you cloned a while ago, get the latest changes from upstream: 93 | 94 | ```bash 95 | git checkout 96 | git pull upstream 97 | ``` 98 | 99 | 3. Create a new topic branch off the `development` branch to 100 | contain your feature, change, or fix: 101 | 102 | ```bash 103 | git checkout -b 104 | ``` 105 | 106 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 107 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 108 | or your code is unlikely to be merged into the main project. Use Git's 109 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 110 | feature to tidy up your commits before making them public. 111 | 112 | 4a. Create tests. 113 | 114 | 4b. Create or update the example code that demonstrates the functionality of this change to the code. 115 | 116 | 5. Locally merge (or rebase) the upstream `development` branch into your topic branch: 117 | 118 | ```bash 119 | git pull [--rebase] upstream development 120 | ``` 121 | 122 | 6. Push your topic branch up to your fork: 123 | 124 | ```bash 125 | git push origin 126 | ``` 127 | 128 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 129 | with a clear title and description against the `development` branch. All tests must be passing before we will review the PR. 130 | 131 | ## Code Reviews 132 | If you can, please look at open PRs and review them. 133 | Give feedback and help us merge these PRs much faster! 134 | If you don't know how, GitHub has some great 135 | [information on how to review a Pull Request](https://help.github.com/articles/about-pull-request-reviews/). 136 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | ENV PYTHON_VERSIONS='python2.6 python2.7 python3.4 python3.5 python3.6' \ 3 | OAI_SPEC_URL="https://raw.githubusercontent.com/sendgrid/sendgrid-oai/HEAD/oai_stoplight.json" 4 | 5 | # install testing versions of python, including old versions, from deadsnakes 6 | RUN set -x \ 7 | && apt-get update \ 8 | && apt-get install -y --no-install-recommends software-properties-common \ 9 | && apt-add-repository -y ppa:fkrull/deadsnakes \ 10 | && apt-get update \ 11 | && apt-get install -y --no-install-recommends $PYTHON_VERSIONS \ 12 | git \ 13 | curl \ 14 | && apt-get purge -y --auto-remove software-properties-common \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | WORKDIR /root 18 | 19 | # install Prism 20 | ADD https://raw.githubusercontent.com/stoplightio/prism/HEAD/install.sh install.sh 21 | RUN chmod +x ./install.sh && \ 22 | ./install.sh && \ 23 | rm ./install.sh 24 | 25 | # install pip, tox 26 | ADD https://bootstrap.pypa.io/get-pip.py get-pip.py 27 | RUN python2.7 get-pip.py && \ 28 | pip install tox && \ 29 | rm get-pip.py 30 | 31 | # set up default sendgrid env 32 | WORKDIR /root/sources 33 | RUN git clone https://github.com/sendgrid/sendgrid-python.git && \ 34 | git clone https://github.com/sendgrid/python-http-client.git 35 | WORKDIR /root 36 | RUN ln -s /root/sources/sendgrid-python/sendgrid && \ 37 | ln -s /root/sources/python-http-client/python_http_client 38 | 39 | COPY . . 40 | CMD sh run.sh 41 | -------------------------------------------------------------------------------- /FIRST_TIMERS.md: -------------------------------------------------------------------------------- 1 | # How To Contribute to Twilio SendGrid Repositories via GitHub 2 | Contributing to the Twilio SendGrid repositories is easy! All you need to do is find an open issue (see the bottom of this page for a list of repositories containing open issues), fix it and submit a pull request. Once you have submitted your pull request, the team can easily review it before it is merged into the repository. 3 | 4 | To make a pull request, follow these steps: 5 | 6 | 1. Log into GitHub. If you do not already have a GitHub account, you will have to create one in order to submit a change. Click the Sign up link in the upper right-hand corner to create an account. Enter your username, password, and email address. If you are an employee of Twilio SendGrid, please use your full name with your GitHub account and enter Twilio SendGrid as your company so we can easily identify you. 7 | 8 | 9 | 10 | 2. __[Fork](https://help.github.com/fork-a-repo/)__ the [smtpapi-python](https://github.com/sendgrid/smtpapi-python) repository: 11 | 12 | 13 | 14 | 3. __Clone__ your fork via the following commands: 15 | 16 | ```bash 17 | # Clone your fork of the repo into the current directory 18 | git clone https://github.com/your_username/smtpapi-python 19 | # Navigate to the newly cloned directory 20 | cd smtpapi-python 21 | # Assign the original repo to a remote called "upstream" 22 | git remote add upstream https://github.com/sendgrid/smtpapi-python 23 | ``` 24 | 25 | > Don't forget to replace *your_username* in the URL by your real GitHub username. 26 | 27 | 4. __Create a new topic branch__ (off the main project development branch) to contain your feature, change, or fix: 28 | 29 | ```bash 30 | git checkout -b 31 | ``` 32 | 33 | 5. __Commit your changes__ in logical chunks. 34 | 35 | Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up your commits before making them public. Probably you will also have to create tests (if needed) or create or update the example code that demonstrates the functionality of this change to the code. 36 | 37 | 6. __Locally merge (or rebase)__ the upstream development branch into your topic branch: 38 | 39 | ```bash 40 | git pull [--rebase] upstream main 41 | ``` 42 | 43 | 7. __Push__ your topic branch up to your fork: 44 | 45 | ```bash 46 | git push origin 47 | ``` 48 | 49 | 8. __[Open a Pull Request](https://help.github.com/articles/creating-a-pull-request/#changing-the-branch-range-and-destination-repository/)__ with a clear title and description against the `main` branch. All tests must be passing before we will review the PR. 50 | 51 | ## Important notice 52 | 53 | Before creating a pull request, make sure that you respect the repository's constraints regarding contributions. You can find them in the [CONTRIBUTING.md](CONTRIBUTING.md) file. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2023, Twilio SendGrid, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE VERSION.txt 2 | recursive-include smtpapi *.py *.txt 3 | prune test 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: venv install test-install test clean nopyc 2 | 3 | venv: 4 | @python --version || (echo "Python is not installed, please install Python 2 or Python 3"; exit 1); 5 | pip install virtualenv 6 | virtualenv --python=python venv 7 | 8 | install: venv 9 | . venv/bin/activate; pip install . 10 | 11 | test-install: 12 | . venv/bin/activate; pip install -r test/requirements.txt 13 | 14 | test: 15 | . venv/bin/activate; python -m unittest discover -v 16 | . venv/bin/activate; python test/__init__.py 17 | . venv/bin/activate; flake8 --statistics --count 18 | . venv/bin/activate; coverage run test/__init__.py 19 | 20 | clean: nopyc 21 | rm -rf venv 22 | 23 | nopyc: 24 | find . -name \*.pyc -delete 25 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Fixes # 18 | 19 | A short description of what this PR does. 20 | 21 | ### Checklist 22 | - [x] I acknowledge that all my contributions will be made under the project's license 23 | - [ ] I have made a material change to the repo (functionality, testing, spelling, grammar) 24 | - [ ] I have read the [Contribution Guidelines](https://github.com/sendgrid/smtpapi-python/blob/main/CONTRIBUTING.md) and my PR follows them 25 | - [ ] I have titled the PR appropriately 26 | - [ ] I have updated my branch with the main branch 27 | - [ ] I have added tests that prove my fix is effective or that my feature works 28 | - [ ] I have added the necessary documentation about the functionality in the appropriate .md file 29 | - [ ] I have added inline documentation to the code I modified 30 | 31 | If you have questions, please file a [support ticket](https://support.sendgrid.com). 32 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://uiux.s3.amazonaws.com/2016-logos/email-logo%402x.png 2 | :target: https://www.sendgrid.com 3 | :alt: SendGrid Logo 4 | 5 | |Test and Deploy Badge| |Twitter Follow| |Codecov branch| |Python Versions| |PyPI Version| |GitHub contributors| |MIT Licensed| 6 | 7 | **This module helps build SendGrid's SMTP API headers.** 8 | 9 | Learn more about the SMTP API at `SendGrid documentation`_. 10 | 11 | Announcements 12 | ============= 13 | All the updates to this module are documented in our `CHANGELOG`_. 14 | 15 | Table of Contents 16 | ================= 17 | 18 | - `Installation <#installation>`__ 19 | - `Quick Start <#quick-start>`__ 20 | - `Usage <#usage>`__ 21 | - `How to Contribute <#how-to-contribute>`__ 22 | - `Local Setup of the Project <#local-setup-of-the-project>`__ 23 | - `About <#about>`__ 24 | - `License <#license>`__ 25 | 26 | Installation 27 | ============ 28 | 29 | Prerequisites 30 | ------------- 31 | 32 | - Python version 2.7 or 3.4+ 33 | - The SendGrid service, starting at the `free level`_ 34 | 35 | Install Package 36 | --------------- 37 | 38 | .. code:: bash 39 | 40 | pip install smtpapi 41 | 42 | Setup Environment Variables 43 | --------------------------- 44 | 45 | Update the development environment with your `SENDGRID_API_KEY`_, for example: 46 | 47 | .. code:: bash 48 | 49 | cp .env_sample .env 50 | 51 | In ``.env`` set ``SENDGRID_API_KEY`` to your own API key. 52 | 53 | You can add your environment variables to your environment by sourcing the file: 54 | 55 | .. code:: bash 56 | 57 | source .env 58 | 59 | Quick Start 60 | =========== 61 | 62 | .. code:: python 63 | 64 | from smtpapi import SMTPAPIHeader 65 | 66 | header = SMTPAPIHeader() 67 | header.add_to('email@email.com') 68 | print(header.json_string()) 69 | 70 | Usage 71 | ===== 72 | 73 | - `SendGrid documentation`_ 74 | - `Example Code`_ 75 | 76 | How to Contribute 77 | ================= 78 | 79 | We encourage contribution to our projects, please see our `CONTRIBUTING`_ guide for more details. 80 | 81 | Quick links: 82 | 83 | - `Improvements to the Codebase`_ 84 | - `Review Pull Requests`_ 85 | 86 | Local Setup of the Project 87 | ========================== 88 | 89 | The simplest local development workflow is by using Docker. 90 | 91 | Steps: 92 | 93 | 1. Install [Docker](https://www.docker.com/) 94 | 2. Run ``docker-compose build`` (this builds the container) 95 | 3. Run ``docker-compose up`` (this runs tests by default) 96 | 97 | About 98 | ===== 99 | 100 | **smtpapi-python** is maintained and funded by Twilio SendGrid, Inc. 101 | The names and logos for **smtpapi-python** are trademarks of Twilio SendGrid, Inc. 102 | 103 | License 104 | ======= 105 | 106 | `The MIT License (MIT)`_ 107 | 108 | .. _SendGrid documentation: https://sendgrid.com/docs/API_Reference/SMTP_API/index.html 109 | .. _CHANGELOG: https://github.com/sendgrid/smtpapi-python/blob/HEAD/CHANGELOG.md 110 | .. _free level: https://sendgrid.com/free?source=sendgrid-python 111 | .. _SENDGRID_API_KEY: https://app.sendgrid.com/settings/api_keys 112 | .. _Example Code: https://github.com/sendgrid/smtpapi-python/tree/HEAD/examples 113 | .. _CONTRIBUTING: https://github.com/sendgrid/smtpapi-python/blob/HEAD/CONTRIBUTING.md 114 | .. _Improvements to the Codebase: https://github.com/sendgrid/smtpapi-python/blob/HEAD/CONTRIBUTING.md#improvements-to-the-codebase 115 | .. _Review Pull Requests: https://github.com/sendgrid/smtpapi-python/blob/HEAD/CONTRIBUTING.md#code-reviews) 116 | .. _The MIT License (MIT): https://github.com/sendgrid/smtpapi-python/blob/HEAD/LICENSE 117 | 118 | .. |Test and Deploy Badge| image:: https://github.com/sendgrid/smtpapi-python/actions/workflows/test-and-deploy.yml/badge.svg 119 | :target: https://github.com/sendgrid/smtpapi-python/actions/workflows/test-and-deploy.yml 120 | .. |Twitter Follow| image:: https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow 121 | :target: https://twitter.com/sendgrid 122 | .. |Codecov branch| image:: https://img.shields.io/codecov/c/github/sendgrid/smtpapi-python/main.svg?style=flat-square&label=Codecov+Coverage 123 | :target: https://codecov.io/gh/sendgrid/smtpapi-python 124 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/smtpapi.svg 125 | :target: https://pypi.org/project/smtpapi/ 126 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/smtpapi.svg 127 | :target: https://pypi.org/project/smtpapi/ 128 | .. |GitHub contributors| image:: https://img.shields.io/github/contributors/sendgrid/smtpapi-python.svg 129 | :target: https://github.com/sendgrid/smtpapi-python/graphs/contributors 130 | .. |MIT Licensed| image:: https://img.shields.io/badge/license-MIT-blue.svg 131 | :target: https://github.com/sendgrid/smtpapi-python/blob/HEAD/LICENSE 132 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | If you have a non-library SendGrid issue, please contact our [support team](https://support.sendgrid.com). 2 | 3 | ## Table of Contents 4 | 5 | * [Viewing the Request Body](#request-body) 6 | 7 | 8 | ## Viewing the Request Body 9 | 10 | When debugging or testing, it may be useful to examine the raw request body to compare against the [documented format](https://sendgrid.com/docs/API_Reference/api_v3.html). 11 | 12 | You can do this like so: 13 | 14 | ```python 15 | print(mail.get()) 16 | ``` 17 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | This documentation is based on our [API_Reference](https://sendgrid.com/docs/API_Reference/SMTP_API/index.html) 2 | 3 | # INITIALIZATION 4 | 5 | ## Simple Way 6 | 7 | ```python 8 | from smtpapi import SMTPAPIHeader 9 | 10 | header = SMTPAPIHeader() 11 | header.set_tos(['test1@example.com', 'test2@example.com']) 12 | print(header.json_string()) 13 | ``` 14 | 15 | or 16 | 17 | ## With Several SMTP-API Methods 18 | 19 | ```python 20 | 21 | from smtpapi import SMTPAPIHeader 22 | 23 | header = SMTPAPIHeader() 24 | header.set_tos(['test1@example.com', 'test2@example.com']) 25 | header.set_categories(['category1', 'category2']) 26 | header.set_send_at(int(time.time()) 27 | header.set_sections({'key1':'section1', 'key2':'section2'}) 28 | header.set_substitutions({'key': ['value1', 'value2']}) 29 | header.set_asm_group_id('value') 30 | header.set_unique_args({'key':'value'}) 31 | print(header.json_string()) 32 | ``` 33 | 34 | # Table of Contents 35 | 36 | * [Using the SMTP API](#use-smtp-api) 37 | * [Settings](#settings) 38 | * [Categories](#categories) 39 | * [Scheduling Parameters](#scheduling-parameters) 40 | * [Section Tags](#section-tags) 41 | * [Substitution Tags](#substitution-tags) 42 | * [Suppression Groups](#suppression-groups) 43 | * [Unique Arguments](#unique-arguments) 44 | 45 | 46 | # Using the SMTP API 47 | 48 | **This endpoint allows you to add as many of the SMTP API methods as you want to a single large JSON string, and pass that JSON string to SendGrid with your messages.** 49 | 50 | You can add as many of the SMTP API methods as you want to a single large JSON string, and pass that JSON string to SendGrid with your messages. To do this, add the JSON string to your message under a header named “X-SMTPAPI” like this: 51 | 52 | ```python 53 | { 54 | "to": [ 55 | "ben@sendgrid.com", 56 | "joe@sendgrid.com" 57 | ], 58 | "sub": { 59 | "%name%": [ 60 | "Ben", 61 | "Joe" 62 | ], 63 | "%role%": [ 64 | "%sellerSection%", 65 | "%buyerSection%" 66 | ] 67 | }, 68 | "section": { 69 | "%sellerSection%": "Seller information for: %name%", 70 | "%buyerSection%": "Buyer information for: %name%" 71 | }, 72 | "category": [ 73 | "Orders" 74 | ], 75 | "unique_args": { 76 | "orderNumber": "12345", 77 | "eventID": "6789" 78 | }, 79 | "filters": { 80 | "footer": { 81 | "settings": { 82 | "enable": 1, 83 | "text/plain": "Thank you for your business" 84 | } 85 | } 86 | }, 87 | "send_at": 1409348513 88 | } 89 | ``` 90 | The above example is formatted for readability. Headers must be wrapped to keep the line length under 72. By RFC 821 no line can be longer than 1,000, so if you are going to generate this string yourself it is a good idea to make sure that you wrap it. 91 | 92 | ## Requirements and Limitations 93 | While there is a hard limit of 10,000 addresses that can be sent to in a multiple recipient e-mail, it is best to split up large jobs to around 1,000 recipients, to better allow for the processing load to be distributed. Furthermore, if you have numerous additional substitutions or sections in the headers, it is best to split the group into even smaller groups. 94 | 95 | 96 | # Settings (Filters) 97 | 98 | Following are the settings that can be specified in the filters section of the X-SMTPAPI header. All filters and setting names must be lowercase. 99 | 100 | * If you’re enabling a Setting, also called a filter, via SMTPAPI, you are required to define all of the parameters for that Setting. 101 | * Setting enabled status will always default to your settings on the website, unless otherwise defined in your X-SMTPAPI header 102 | * If you enable a disabled setting, our system will not pull your settings for the disabled setting. You will need to define the settings in your X-SMTPAPI header Example: If you have a footer designed but disabled, you can’t just enable it via the API; you need to define the footer in the API call itself. 103 | * All filter names and setting names must be lowercase. 104 | 105 | ## Filter: `bcc` 106 | Sends a BCC copy of the email created in this transaction to the address specified. 107 | ```python 108 | { 109 | "filters" : { 110 | "bcc" : { 111 | "settings" : { 112 | "enable" : 1, 113 | "email" : "you@example.com" 114 | } 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | Sends a BCC copy of the email created in this transaction to the address specified. 121 | 122 | 123 | # Categories 124 | **This endpoint allows you to add categories to the X-SMTPAPI header of the emails you send via SendGrid** 125 | 126 | By adding categories to the X-SMTPAPI header of the emails that you send via SendGrid you can to track emails based on your categorization system. 127 | 128 | Categories must be in 7bit encoding using the US-ASCII character set, and should be used to group messages together by broad topic. If you need to attach unique data or identifiers to a message, use [Unique Arguments](https://sendgrid.com/docs/API_Reference/SMTP_API/unique_arguments.html) instead. 129 | 130 | ## Example 131 | You can use SendGrid’s SMTP API to add these categories to your email. The following should be added to the email’s header: 132 | 133 | ### Example Category Header 134 | ```python 135 | { 136 | "category": "Example Category" 137 | } 138 | ``` 139 | In this example, SendGrid would associate statistics for the email containing that header with the category Example Category. 140 | 141 | ### Limitations 142 | You can assign up to 10 categories per message: 143 | ```python 144 | { 145 | "category": [ 146 | "dogs", 147 | "animals", 148 | "pets", 149 | "mammals" 150 | ] 151 | } 152 | ``` 153 | 154 | 155 | # Scheduling Parameters 156 | **This endpoint allows you to send large volumes of email in queued batches or target individual recipients by specifying a custom UNIX timestamp parameter.** 157 | 158 | This parameter allows SendGrid to begin processing a customer’s email requests before sending. SendGrid will then queue those messages and release them when the timestamp is exceeded. This technique allows for a more efficient way to distribute large email requests and can improve overall mail delivery time performance. 159 | 160 | The scheduling parameters functionality: 161 | 162 | * Improves efficiency of processing and distributing large volumes of email. 163 | * Reduces email pre-processing time. 164 | * Enables user to set email arrival time to increase open rates. 165 | 166 | Using the parameters defined below, you can queue batches of emails targeting individual recipients. 167 | 168 | **Note: Using both send_at and send_each_at is not valid and will cause your request to be dropped.** 169 | 170 | ### Send At 171 | To schedule a send request for a large batch of emails, use the send_at parameter which will send all emails at approximately the same time. send_at is a UNIX timestamp. 172 | 173 | Example of **`send_at`** email header 174 | 175 | ```python 176 | { 177 | "send_at": 1409348513 178 | } 179 | ``` 180 | 181 | ### Send Each At 182 | To schedule a send request for individual recipients; use send_each_at to send emails to each recipient at the specified time. send_each_at is a sequence of UNIX timestamps, provided as an array. There must be one timestamp per email you wish to send. 183 | 184 | Example of **`send_each_at`** email header 185 | 186 | ```python 187 | { 188 | "to": [ 189 | "", 190 | "john@example.com", 191 | "mike@example.com" 192 | ], 193 | "send_each_at": [ 194 | 1409348513, 195 | 1409348514, 196 | 1409348515 197 | ] 198 | } 199 | ``` 200 | 201 | 202 | # Section Tags 203 | Section tags are similar to substitution tags in how they’re built, but are specific to the message, not the recipient. You have to have a substitution tag value for each recipient, but you can have any number of section tags. Section tags can then contain Substitution tags for the recipient if needed. Section tags have to be contained within a Substitution tag, since SendGrid needs to know which data to populate for the recipient. 204 | 205 | The format of the SMTP API section tag has the form: 206 | ```python 207 | { 208 | "section": { 209 | ":sectionName1": "section 1 text", 210 | ":sectionName2": "section 2 text" 211 | } 212 | } 213 | ``` 214 | 215 | 216 | # Substitution Tags 217 | 218 | **This endpoint allows you to easily generate dynamic content for each recipient on your list.** 219 | 220 | When you send to a list of recipients over SMTP API you can specify substitution tags specific to each recipient. For example, a first name that will then be inserted into an opening greeting like the following, where each recipient sees -firstName- replaced with their first name. 221 | 222 | `"Dear -firstName-"` 223 | 224 | These tags can also be used in more complex scenarios. For example, you could use a -customerID- to build a custom URL that is specific to that user. 225 | 226 | A customer specific ID can replace -customerID- in the URL within your email 227 | `Claim your offer!` 228 | 229 | ## Substitution Tag Example 230 | 231 | Email HTML content: 232 | ``` 233 | 234 | 235 | 236 |

Hello -name-,
237 | Thank you for your interest in our products. I have set up an appointment 238 | to call you at -time- EST to discuss your needs in more detail. If you would 239 | like to reschedule this call please visit the following link: 240 | reschedule 241 | 242 | Regards, 243 | 244 | -salesContact- 245 | -contactPhoneNumber-
246 |

247 | 248 | 249 | ``` 250 | 251 | An accompanying SMTP API JSON header might look something like this: 252 | ``` 253 | { 254 | "to": [ 255 | "john.doe@gmail.com", 256 | "jane.doe@hotmail.com" 257 | ], 258 | "sub": { 259 | "-name-": [ 260 | "John", 261 | "Jane" 262 | ], 263 | "-customerID-": [ 264 | "1234", 265 | "5678" 266 | ], 267 | "-salesContact-": [ 268 | "Jared", 269 | "Ben" 270 | ], 271 | "-contactPhoneNumber-": [ 272 | "555.555.5555", 273 | "777.777.7777" 274 | ], 275 | "-time-": [ 276 | "3:00pm", 277 | "5:15pm" 278 | ] 279 | } 280 | } 281 | ``` 282 | 283 | The resulting email for John would look like this: 284 | ``` 285 | 286 | 287 | 288 |

Hello John,
289 | Thank you for your interest in our products. I have set up an appointment 290 | to call you at 3:00pm EST to discuss your needs in more detail. If you would 291 | like to reschedule this call please visit the following link: 292 | reschedule 293 | 294 | Regards, 295 | 296 | Jared 297 | 555.555.5555
298 |

299 | 300 | 301 | ``` 302 | 303 | 304 | # Suppression Groups 305 | 306 | ## Defining an Unsubscribe Group When Sending 307 | 308 | **This endpoint allows you to specify an unsubscribe group for an email depends on how you will be sending that email.** 309 | 310 | Precaution: 311 | 312 | * When sending an SMTP message, add the group’s ID to the X-SMTPAPI header. 313 | * When sending an email via the Web API v2, add the group’s ID in the `x-smtpapi` parameter. 314 | * When sending an email via the Web API v3, define the group’s ID in the `asm.group_id` parameter. 315 | 316 | You may only specify one group per send, and you should wait one minute after creating the group before sending with it. 317 | 318 | ```python 319 | { 320 | "asm_group_id": 1 321 | } 322 | ``` 323 | 324 | Defining Unsubscribe Groups to display on the Manage Preferences page 325 | To specify which groups to display on the Manage Preferences page of an email, add the group IDs to the X-SMTPAPI header of an SMTP message, or in the x-smtpapi parameter of a mail.send API call. If the asm_groups_to_display header is omitted, your default groups will be shown on the Manage Preferences page instead. 326 | 327 | You can specify up to 25 groups to display. 328 | ```python 329 | { 330 | "asm_groups_to_display": [1, 2, 3] 331 | } 332 | ``` 333 | 334 | ## Groups 335 | You can find your group IDs by looking at the Group ID column in the Unsubscribe Groups UI, or by calling the [GET method](https://sendgrid.com/docs/API_Reference/Web_API_v3/Suppression_Management/groups.html#-GET) of the group's resource. 336 | 337 | 338 | # Unique Arguments 339 | 340 | The SMTP API JSON string allows you to attach an unlimited number of unique arguments to your email up to 10,000 bytes. The arguments are used only for tracking. They can be retrieved through the Event API or the Email Activity page. 341 | 342 | These arguments can be added using a JSON string like this: 343 | ``` 344 | { 345 | "unique_args": { 346 | "customerAccountNumber": "55555", 347 | "activationAttempt": "1", 348 | "New Argument 1": "New Value 1", 349 | "New Argument 2": "New Value 2", 350 | "New Argument 3": "New Value 3", 351 | "New Argument 4": "New Value 4" 352 | } 353 | } 354 | ``` 355 | 356 | These arguments can then be seen in posts from the SendGrid Event Webhook. The contents of one of these POST requests would look something like this: 357 | 358 | ## Example Webhook Post Data 359 | 360 | ``` 361 | { 362 | "sg_message_id": "145cea24eb8.1c420.57425.filter-132.3382.5368192A3.0", 363 | "New Argument 1": "New Value 1", 364 | "event": "processed", 365 | "New Argument 4": "New Value 4", 366 | "email": "user@example.com", 367 | "smtp-id": "<145cea24eb8.1c420.57425@localhost.localdomain>", 368 | "timestamp": 1399331116, 369 | "New Argument 2": "New Value 2", 370 | "New Argument 3": "New Value 3", 371 | "customerAccountNumber": "55555", 372 | "activationAttempt": "1" 373 | } 374 | ``` 375 | Unique Arguments will also be shown in the Email Activity tab of your account. 376 | 377 | To apply different unique arguments to individual emails, you may use substitution tags. An example of this would look like: 378 | ``` 379 | { 380 | "sub": { 381 | "-account_number-": [ 382 | "314159", 383 | "271828" 384 | ] 385 | }, 386 | "unique_args": { 387 | "customerAccountNumber": "-account_number-" 388 | } 389 | } 390 | ``` 391 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.4.12 2 | -------------------------------------------------------------------------------- /changes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Small python script that, when run, will update the CHANGELOG with information 4 | about all merged pull requests since the previous release. 5 | 6 | This script must be run after tagging the latest version 7 | It checks the log of commits since the previous tag and parses it 8 | """ 9 | import re 10 | import subprocess 11 | import sys 12 | from datetime import datetime 13 | 14 | # Regex patterns 15 | RELEASE_MD_PATTERN = re.compile(r'## \[(\d+\.\d+\.\d+)\]') 16 | MERGED_PR_PATTERN = re.compile( 17 | r'([0-9a-f]{7}) Merge pull request #(\d+) from (.+)/.+' 18 | ) 19 | TAG_PATTERN = re.compile( 20 | r'refs/tags/v(\d+\.\d+\.\d+) (\w{3} \w{3} \d{1,2} \d{2}:\d{2}:\d{2} \d{4})' 21 | ) 22 | 23 | # PR Type terms 24 | FIX_TERMS = ['fix', 'change', 'update'] 25 | 26 | 27 | # Helper functions 28 | def generate_pr_link(pr_num): 29 | """ 30 | Returns a markdown link to a PR in this repo given its number 31 | """ 32 | return ( 33 | '[PR #{0}](https://github.com/sendgrid/smtpapi-python/pulls/{0})' 34 | ).format(pr_num) 35 | 36 | 37 | def generate_user_link(user): 38 | """ 39 | Returns a markdown link to a user 40 | """ 41 | return '[@{0}](https://github.com/{0})'.format(user) 42 | 43 | 44 | # Get latest tag 45 | command = ['git', 'tag', '--format=%(refname) %(creatordate)'] 46 | res = subprocess.run(command, capture_output=True, text=True) 47 | if res.returncode != 0: 48 | print('Error occurred when running git tag command:', str(res.stderr)) 49 | sys.exit(1) 50 | # Get the last line and get the tag number 51 | latest_release_match = TAG_PATTERN.match( 52 | list(filter(None, res.stdout.split('\n')))[-1], 53 | ) 54 | latest_release = latest_release_match[1] 55 | latest_release_date = datetime.strptime( 56 | latest_release_match[2], '%a %b %d %H:%M:%S %Y', 57 | ) 58 | print('Generating CHANGELOG for', latest_release) 59 | 60 | # Read in the CHANGELOG file first 61 | with open('CHANGELOG.md') as f: 62 | # Read the text in as a list of lines 63 | old_text = f.readlines() 64 | # Get the latest release (top of the CHANGELOG) 65 | for line in old_text: 66 | match = RELEASE_MD_PATTERN.match(line) 67 | if match: 68 | prev_release = match[1] 69 | break 70 | 71 | if latest_release == prev_release: 72 | print( 73 | 'The latest git tag matches the last release in the CHANGELOG. ' 74 | 'Please tag the repository before running this script.' 75 | ) 76 | sys.exit(1) 77 | 78 | # Use git log to list all commits between that tag and HEAD 79 | command = 'git log --oneline v{}..@'.format(prev_release).split(' ') 80 | res = subprocess.run(command, capture_output=True, text=True) 81 | if res.returncode != 0: 82 | print('Error occurred when running git log command:', str(res.stderr)) 83 | sys.exit(1) 84 | 85 | # Parse the output from the above command to find all commits for merged PRs 86 | merge_commits = [] 87 | for line in res.stdout.split('\n'): 88 | match = MERGED_PR_PATTERN.match(line) 89 | if match: 90 | merge_commits.append(match) 91 | 92 | # Determine the type of PR from the commit message 93 | added, fixes = [], [] 94 | for commit in merge_commits: 95 | # Get the hash of the commit and get the message of it 96 | commit_sha = commit[1] 97 | command = 'git show {} --format=format:%B'.format(commit_sha).split(' ') 98 | res = subprocess.run(command, capture_output=True, text=True) 99 | out = res.stdout.lower() 100 | is_added = True 101 | 102 | # When storing we need the PR title, number and user 103 | data = { 104 | # 3rd line of the commit message is the PR title 105 | 'title': out.split('\n')[2], 106 | 'number': commit[2], 107 | 'user': commit[3], 108 | } 109 | 110 | for term in FIX_TERMS: 111 | if term in out: 112 | fixes.append(data) 113 | is_added = False 114 | break 115 | if is_added: 116 | added.append(data) 117 | 118 | # Now we need to write out the CHANGELOG again 119 | with open('CHANGELOG.md', 'w') as f: 120 | # Write out the header lines first 121 | for i in range(0, 3): 122 | f.write(old_text[i]) 123 | 124 | # Create and write out the new version information 125 | latest_release_date_string = latest_release_date.strftime('%Y-%m-%d') 126 | f.write('## [{}] - {} ##\n'.format( 127 | latest_release, 128 | latest_release_date_string, 129 | )) 130 | # Add the stuff that was added 131 | f.write('### Added\n') 132 | for commit in added: 133 | f.write('- {}: {}{} (via {})\n'.format( 134 | generate_pr_link(commit['number']), 135 | commit['title'], 136 | '.' if commit['title'][-1] != '.' else '', 137 | generate_user_link(commit['user']) 138 | )) 139 | f.write('\n') 140 | # Add the fixes 141 | f.write('### Fixes\n') 142 | for commit in fixes: 143 | f.write('- {}: {}{} (via {})\n'.format( 144 | generate_pr_link(commit['number']), 145 | commit['title'], 146 | '.' if commit['title'][-1] != '.' else '', 147 | generate_user_link(commit['user']) 148 | )) 149 | f.write('\n') 150 | 151 | # Add the old stuff 152 | for i in range(3, len(old_text)): 153 | f.write(old_text[i]) 154 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | web: 4 | build: . 5 | volumes: 6 | - .:/root 7 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | # Python 2/3 compatible codebase 2 | from __future__ import absolute_import, division, print_function 3 | from smtpapi import SMTPAPIHeader 4 | 5 | import time 6 | from os import path, sys 7 | 8 | if __name__ == '__main__' and __package__ is None: 9 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 10 | 11 | header = SMTPAPIHeader() 12 | 13 | # [To](http://sendgrid.com/docs/API_Reference/SMTP_API/index.html) 14 | # header.add_to('test@example.com') 15 | header.set_tos(['test1@example.com', 'test2@example.com']) 16 | 17 | # [Substitutions] 18 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/substitution_tags.html) 19 | # header.add_substitution('key', 'value') 20 | header.set_substitutions({'key': ['value1', 'value2']}) 21 | 22 | # [Unique Arguments] 23 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/unique_arguments.html) 24 | # header.add_unique_arg('key', 'value') 25 | header.set_unique_args({'key': 'value'}) 26 | 27 | # [Categories](http://sendgrid.com/docs/API_Reference/SMTP_API/categories.html) 28 | # header.add_category('category') 29 | header.set_categories(['category1', 'category2']) 30 | 31 | # [Sections](http://sendgrid.com/docs/API_Reference/SMTP_API/section_tags.html) 32 | # header.add_section('key', 'section') 33 | header.set_sections({'key1': 'section1', 'key2': 'section2'}) 34 | 35 | # [Filters] 36 | # (http://sendgrid.com/docs/API_Reference/SMTP_API/apps.html) 37 | header.add_filter('filter', 'setting', 'value') 38 | 39 | # [ASM Group ID] 40 | # (https://sendgrid.com/docs/User_Guide/advanced_suppression_manager.html) 41 | header.set_asm_group_id('value') 42 | 43 | # [IP Pools] 44 | # (https://sendgrid.com/docs/API_Reference/Web_API_v3/IP_Management/ip_pools.html) 45 | header.set_ip_pool("testPool") 46 | 47 | # [Scheduling Parameters] 48 | # (https://sendgrid.com/docs/API_Reference/SMTP_API/scheduling_parameters.html) 49 | # header.add_send_each_at(unix_timestamp) # must be a unix timestamp 50 | # header.set_send_each_at([]) # must be a unix timestamp 51 | header.set_send_at(int(time.time())) # must be a unix timestamp 52 | 53 | print(header.json_string()) 54 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python2.7 setup.py install 4 | python2.7 test/__init__.py 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from distutils.file_util import copy_file 4 | from setuptools import setup, find_packages 5 | 6 | 7 | dir_path = os.path.abspath(os.path.dirname(__file__)) 8 | readme = io.open(os.path.join(dir_path, 'README.rst'), encoding='utf-8').read() 9 | version = io.open( 10 | os.path.join(dir_path, 'VERSION.txt'), 11 | encoding='utf-8', 12 | ).read().strip() 13 | copy_file(os.path.join(dir_path, 'VERSION.txt'), 14 | os.path.join(dir_path, 'smtpapi', 'VERSION.txt'), 15 | verbose=0) 16 | setup( 17 | name='smtpapi', 18 | version=version, 19 | author='Yamil Asusta, Kane Kim', 20 | author_email='yamil@sendgrid.com, kane.isturm@sendgrid.com', 21 | url='https://github.com/sendgrid/smtpapi-python/', 22 | packages=find_packages(exclude=["test"]), 23 | include_package_data=True, 24 | license='MIT License', 25 | description='Simple wrapper to use SendGrid SMTP API', 26 | long_description=readme, 27 | classifiers=[ 28 | 'Programming Language :: Python :: 2.7', 29 | 'Programming Language :: Python :: 3.4', 30 | 'Programming Language :: Python :: 3.5', 31 | 'Programming Language :: Python :: 3.6', 32 | 'Programming Language :: Python :: 3.7', 33 | 'Programming Language :: Python :: 3.8', 34 | 'Programming Language :: Python :: 3.9', 35 | 'Programming Language :: Python :: 3.10', 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /smtpapi/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import decimal 3 | 4 | 5 | class _CustomJSONEncoder(json.JSONEncoder): 6 | 7 | def default(self, o): 8 | if isinstance(o, decimal.Decimal): 9 | return float(o) 10 | # Provide a fallback to the default encoder if we haven't implemented 11 | # special support for the object's class 12 | return super(_CustomJSONEncoder, self).default(o) 13 | 14 | 15 | class SMTPAPIHeader(object): 16 | 17 | def __init__(self): 18 | self.data = {} 19 | 20 | def add_to(self, to): 21 | if 'to' not in self.data: 22 | self.data['to'] = [] 23 | if type(to) is list: 24 | self.data['to'] += to 25 | else: 26 | self.data['to'].append(to) 27 | 28 | def set_tos(self, tos): 29 | self.data['to'] = tos 30 | 31 | def add_substitution(self, key, value): 32 | if 'sub' not in self.data: 33 | self.data['sub'] = {} 34 | if key not in self.data['sub']: 35 | self.data['sub'][key] = [] 36 | self.data['sub'][key].append(value) 37 | 38 | def set_substitutions(self, subs): 39 | self.data['sub'] = subs 40 | 41 | def _add_key_value(self, index, key, value): 42 | if index not in self.data: 43 | self.data[index] = {} 44 | self.data[index][key] = value 45 | 46 | def _add_key(self, index, key): 47 | if index not in self.data: 48 | self.data[index] = [] 49 | self.data[index].append(key) 50 | 51 | def add_unique_arg(self, key, value): 52 | self._add_key_value('unique_args', key, value) 53 | 54 | def set_unique_args(self, value): 55 | self.data['unique_args'] = value 56 | 57 | def add_category(self, category): 58 | self._add_key('category', category) 59 | 60 | def set_categories(self, category): 61 | self.data['category'] = category 62 | 63 | def add_section(self, key, section): 64 | self._add_key_value('section', key, section) 65 | 66 | def set_sections(self, value): 67 | self.data['section'] = value 68 | 69 | def add_send_each_at(self, time): 70 | self._add_key('send_each_at', time) 71 | 72 | def set_send_each_at(self, time): 73 | self.data['send_each_at'] = time 74 | 75 | def set_send_at(self, time): 76 | self.data['send_at'] = time 77 | 78 | def add_filter(self, app, setting, val): 79 | if 'filters' not in self.data: 80 | self.data['filters'] = {} 81 | if app not in self.data['filters']: 82 | self.data['filters'][app] = {} 83 | if 'settings' not in self.data['filters'][app]: 84 | self.data['filters'][app]['settings'] = {} 85 | self.data['filters'][app]['settings'][setting] = val 86 | 87 | def set_asm_group_id(self, value): 88 | if not bool(value): 89 | self.data['asm_group_id'] = {} 90 | else: 91 | self.data['asm_group_id'] = value 92 | 93 | def set_ip_pool(self, value): 94 | if bool(value): 95 | self.data['ip_pool'] = value 96 | else: 97 | self.data['ip_pool'] = {} 98 | 99 | def json_string(self): 100 | result = {} 101 | for key in self.data.keys(): 102 | if self.data[key] != [] and self.data[key] != {}: 103 | result[key] = self.data[key] 104 | return json.dumps(result, cls=_CustomJSONEncoder) 105 | -------------------------------------------------------------------------------- /static/img/github-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/static/img/github-fork.png -------------------------------------------------------------------------------- /static/img/github-sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/static/img/github-sign-up.png -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import json 3 | import os 4 | import datetime 5 | 6 | from smtpapi import SMTPAPIHeader 7 | 8 | try: 9 | import unittest2 as unittest 10 | except ImportError: 11 | import unittest 12 | 13 | 14 | class TestSMTPAPI(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.validHeader = json.loads('''{"to":["test@email.com", 18 | "test2@email.com", "test3@email.com"], 19 | "sub":{"subKey":["subValue"],"decimalKey":[1.23456789]}, 20 | "section":{"testSection":"sectionValue"}, 21 | "category":["testCategory"], 22 | "unique_args":{"testUnique":"uniqueValue"}, 23 | "asm_group_id":42, 24 | "send_each_at":[1409348513, 1409348514], 25 | "send_at": 1409348515, 26 | "ip_pool": "testPool", 27 | "filters":{"testFilter":{"settings":{"filter":"filterValue"}}}}''') 28 | 29 | self.dropsHeader = json.loads('''{ 30 | "sub":{"subKey":["subValue"],"decimalKey":[1.23456789]}, 31 | "unique_args":{"testUnique":"uniqueValue"}, 32 | "filters":{"testFilter":{"settings":{"filter":"filterValue"}}}}''') 33 | 34 | def test_add(self): 35 | header = SMTPAPIHeader() 36 | header.add_to('test@email.com') 37 | header.add_to(['test2@email.com', 'test3@email.com']) 38 | header.add_substitution('subKey', 'subValue') 39 | header.add_substitution('decimalKey', decimal.Decimal("1.23456789")) 40 | header.add_section('testSection', 'sectionValue') 41 | header.add_category('testCategory') 42 | header.add_unique_arg('testUnique', 'uniqueValue') 43 | header.set_asm_group_id(42) 44 | header.add_send_each_at(1409348513) 45 | header.add_send_each_at(1409348514) 46 | header.set_send_at(1409348515) 47 | header.set_ip_pool('testPool') 48 | header.add_filter('testFilter', 'filter', 'filterValue') 49 | self.assertEqual(self.validHeader, json.loads(header.json_string())) 50 | 51 | def test_set(self): 52 | header = SMTPAPIHeader() 53 | header.set_tos([ 54 | "test@email.com", 55 | "test2@email.com", 56 | "test3@email.com", 57 | ]) 58 | header.set_substitutions({ 59 | "subKey": ["subValue"], 60 | "decimalKey": [decimal.Decimal("1.23456789")] 61 | }) 62 | header.set_sections(json.loads('{"testSection":"sectionValue"}')) 63 | header.set_categories(["testCategory"]) 64 | header.set_unique_args(json.loads('{"testUnique":"uniqueValue"}')) 65 | header.set_asm_group_id(42) 66 | header.set_send_each_at([1409348513, 1409348514]) 67 | header.set_send_at(1409348515) 68 | header.set_ip_pool('testPool') 69 | header.add_filter('testFilter', 'filter', 'filterValue') 70 | self.assertEqual(self.validHeader, json.loads(header.json_string())) 71 | 72 | def test_drop_empty(self): 73 | header = SMTPAPIHeader() 74 | header.set_tos([]) 75 | header.set_substitutions({ 76 | "subKey": ["subValue"], 77 | "decimalKey": [decimal.Decimal("1.23456789")] 78 | }) 79 | header.set_sections(json.loads('{}')) 80 | header.set_categories([]) 81 | header.set_unique_args(json.loads('{"testUnique":"uniqueValue"}')) 82 | header.set_asm_group_id(None) 83 | header.set_send_each_at([]) 84 | header.set_ip_pool(None) 85 | header.add_filter('testFilter', 'filter', 'filterValue') 86 | self.assertEqual(self.dropsHeader, json.loads(header.json_string())) 87 | 88 | def test_license_year(self): 89 | LICENSE_FILE = 'LICENSE' 90 | copyright_line = '' 91 | with open(LICENSE_FILE, 'r') as f: 92 | for line in f: 93 | if line.startswith('Copyright'): 94 | copyright_line = line.strip() 95 | break 96 | self.assertEqual( 97 | 'Copyright (C) %s, Twilio SendGrid, Inc. ' 98 | % datetime.datetime.now().year, 99 | copyright_line 100 | ) 101 | 102 | 103 | class TestRepository(unittest.TestCase): 104 | 105 | def setUp(self): 106 | 107 | self.required_files = [ 108 | './Dockerfile', 109 | './.env_sample', 110 | './PULL_REQUEST_TEMPLATE.md', 111 | './.gitignore', 112 | './CHANGELOG.md', 113 | './CODE_OF_CONDUCT.md', 114 | './CONTRIBUTING.md', 115 | './LICENSE', 116 | './README.rst', 117 | './TROUBLESHOOTING.md', 118 | './USAGE.md', 119 | './VERSION.txt', 120 | ] 121 | 122 | self.file_not_found_message = 'File "{0}" does not exist in repo!' 123 | 124 | def test_repository_files_exists(self): 125 | 126 | for file_path in self.required_files: 127 | if isinstance(file_path, list): 128 | # multiple file paths: assert that any one of the files exists 129 | self.assertTrue( 130 | any(os.path.exists(f) for f in file_path), 131 | msg=self.file_not_found_message.format( 132 | '" or "'.join(file_path) 133 | ), 134 | ) 135 | else: 136 | self.assertTrue( 137 | os.path.exists(file_path), 138 | msg=self.file_not_found_message.format(file_path), 139 | ) 140 | 141 | 142 | if __name__ == '__main__': 143 | unittest.main() 144 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | sendgrid 2 | coverage 3 | flake8 4 | -------------------------------------------------------------------------------- /test/test_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | 5 | class ProjectTests(unittest.TestCase): 6 | 7 | # ./Docker or docker/Docker 8 | def test_docker_dir(self): 9 | self.assertTrue( 10 | os.path.isfile("./Dockerfile") 11 | or os.path.isdir("./docker/Dockerfile") 12 | ) 13 | 14 | # ./docker-compose.yml or ./docker/docker-compose.yml 15 | def test_docker_compose(self): 16 | self.assertTrue( 17 | os.path.isfile('./docker-compose.yml') 18 | or os.path.isfile('./docker/docker-compose.yml') 19 | ) 20 | 21 | # ./.env_sample 22 | def test_env(self): 23 | self.assertTrue(os.path.isfile('./.env_sample')) 24 | 25 | # ./.gitignore 26 | def test_gitignore(self): 27 | self.assertTrue(os.path.isfile('./.gitignore')) 28 | 29 | # ./CHANGELOG.md 30 | def test_changelog(self): 31 | self.assertTrue(os.path.isfile('./CHANGELOG.md')) 32 | 33 | # ./CODE_OF_CONDUCT.md 34 | def test_code_of_conduct(self): 35 | self.assertTrue(os.path.isfile('./CODE_OF_CONDUCT.md')) 36 | 37 | # ./CONTRIBUTING.md 38 | def test_contributing(self): 39 | self.assertTrue(os.path.isfile('./CONTRIBUTING.md')) 40 | 41 | # ./LICENSE 42 | def test_license(self): 43 | self.assertTrue(os.path.isfile('./LICENSE')) 44 | 45 | # ./PULL_REQUEST_TEMPLATE.md 46 | def test_pr_template(self): 47 | self.assertTrue( 48 | os.path.isfile('./PULL_REQUEST_TEMPLATE.md') 49 | ) 50 | 51 | # ./README.rst 52 | def test_readme(self): 53 | self.assertTrue(os.path.isfile('./README.rst')) 54 | 55 | # ./TROUBLESHOOTING.md 56 | def test_troubleshooting(self): 57 | self.assertTrue(os.path.isfile('./TROUBLESHOOTING.md')) 58 | 59 | # ./USAGE.md 60 | def test_usage(self): 61 | self.assertTrue(os.path.isfile('./USAGE.md')) 62 | 63 | # ./VERSION.txt 64 | def test_use_cases(self): 65 | self.assertTrue(os.path.isfile('./VERSION.txt')) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /twilio_sendgrid_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-python/e89ee24d62cfc57608369836b0f8fc9a1daddeb3/twilio_sendgrid_logo.png -------------------------------------------------------------------------------- /use_cases/README.md: -------------------------------------------------------------------------------- 1 | This directory provides examples for specific use cases. Please [open an issue](https://github.com/sendgrid/smtpapi-python/issues) or make a pull request for any use cases you would like to see here. Thank you! 2 | 3 | # Table of Contents 4 | --------------------------------------------------------------------------------