diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index ef7e1cb56..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - pep8: - enabled: true diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml new file mode 100644 index 000000000..5dcf0328c --- /dev/null +++ b/.github/workflows/lint-python.yml @@ -0,0 +1,16 @@ +# https://beta.ruff.rs +name: ruff +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: pip install --user ruff + - run: ruff check --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 000000000..31520079c --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,21 @@ +name: Lint PR +on: + pull_request_target: + types: [ opened, edited, synchronize, reopened ] + +jobs: + validate: + name: Validate title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + with: + types: | + chore + docs + fix + feat + misc + test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml new file mode 100644 index 000000000..98bbeefbd --- /dev/null +++ b/.github/workflows/test-and-deploy.yml @@ -0,0 +1,97 @@ +name: Test and Deploy +on: + push: + branches: [ '*' ] + tags: [ '*' ] + pull_request: + branches: [ main ] + schedule: + # Run automatically at 8AM PST Monday-Friday + - cron: '0 15 * * 1-5' + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' , '3.13'] + env: + DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v3 + + - name: Login to Docker Hub + if: env.DOCKER_LOGIN + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_AUTH_TOKEN }} + + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + + - name: Build & Test + run: make test + + deploy: + name: Deploy + if: success() && github.ref_type == 'tag' + needs: [ test ] + runs-on: ubuntu-latest + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install wheel + python setup.py sdist bdist_wheel + + - name: Create GitHub Release + uses: sendgrid/dx-automator/actions/release@main + with: + footer: '**[pypi](https://pypi.org/project/sendgrid/${version})**' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + - name: Submit metric to Datadog + uses: sendgrid/dx-automator/actions/datadog-release-metric@main + env: + DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} + + notify-on-failure: + name: Slack notify on failure + if: failure() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') + needs: [ test, deploy ] + runs-on: ubuntu-latest + steps: + - uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: failure + SLACK_ICON_EMOJI: ':github:' + 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) }} + SLACK_TITLE: Action Failure - ${{ github.repository }} + SLACK_USERNAME: GitHub Actions + SLACK_MSG_AUTHOR: twilio-dx + SLACK_FOOTER: Posted automatically using GitHub Actions + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + MSG_MINIMAL: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8c158b2b3..000000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -dist: xenial # required for Python >= 3.7 -language: python -cache: pip -services: - - docker -env: - matrix: - - version=2.7 - - version=3.5 - - version=3.6 - - version=3.7 - - version=3.8 - global: - - CC_TEST_REPORTER_ID=$TRAVIS_CODE_CLIMATE_TOKEN -before_script: - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - - ./cc-test-reporter before-build -script: - - make test-docker -after_script: - - make test-install - - . venv/bin/activate; codecov - - ./cc-test-reporter after-build --exit-code $? -deploy: - provider: pypi - user: "__token__" - password: $PYPI_TOKEN - skip_cleanup: true - distributions: sdist bdist_wheel - on: - tags: true - condition: $version = 3.6 - -notifications: - slack: - if: branch = main - on_pull_requests: false - on_success: never - on_failure: change - rooms: - - secure: Yp7gJ6NPRPNgO77vwS0HynSdnU5LYlLlUNBEzcx+zy230UxuLLWcYZtIqsIqt4oZm45OwgJLBwoCMgmU2Jcj79rGyqWKYtUcLMLKgHVzSgxjm2outt2fxjXIJHIU60S3RCGofBJRkPwEMb7ibgwHYBEsH3wIeLrVVbWvimxka6A= diff --git a/CHANGELOG.md b/CHANGELOG.md index af7fc752d..67d327649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,217 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-09-19] Version 6.12.5 +--------------------------- +**Library - Fix** +- [PR #1114](https://github.com/sendgrid/sendgrid-python/pull/1114): #1108 - Replace ecdsa with cryptography. Thanks to [@dacevedo12](https://github.com/dacevedo12)! + +**Library - Chore** +- [PR #1117](https://github.com/sendgrid/sendgrid-python/pull/1117): use make-test instead of make test-docker. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + + +[2025-06-12] Version 6.12.4 +--------------------------- +**Library - Chore** +- [PR #1109](https://github.com/sendgrid/sendgrid-python/pull/1109): bug-fix. Thanks to [@manisha1997](https://github.com/manisha1997)! + + +[2025-05-29] Version 6.12.3 +--------------------------- +**Library - Chore** +- [PR #1107](https://github.com/sendgrid/sendgrid-python/pull/1107): export EventWebhookHeader. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! +- [PR #1104](https://github.com/sendgrid/sendgrid-python/pull/1104): fix werkzeug lower versions. Thanks to [@eladkal](https://github.com/eladkal)! +- [PR #1103](https://github.com/sendgrid/sendgrid-python/pull/1103): Relax werkzeug version. Thanks to [@eladkal](https://github.com/eladkal)! + + +[2025-05-13] Version 6.12.2 +--------------------------- +**Library - Chore** +- [PR #1102](https://github.com/sendgrid/sendgrid-python/pull/1102): update ecdsa in setup.py. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + + +[2025-05-13] Version 6.12.1 +--------------------------- +**Library - Fix** +- [PR #1085](https://github.com/sendgrid/sendgrid-python/pull/1085): Vulnerability fix for starkbank-ecdsa 2.2.0 dependency. Thanks to [@ranjanprasad1996](https://github.com/ranjanprasad1996)! + + +[2025-05-05] Version 6.12.0 +--------------------------- +**Library - Chore** +- [PR #1098](https://github.com/sendgrid/sendgrid-python/pull/1098): Add werkzeug package to setup file. Thanks to [@gopidesupavan](https://github.com/gopidesupavan)! +- [PR #1099](https://github.com/sendgrid/sendgrid-python/pull/1099): install docker-compose. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + +**Library - Feature** +- [PR #1087](https://github.com/sendgrid/sendgrid-python/pull/1087): add support for python3.12 and 3.13. Thanks to [@meysam81](https://github.com/meysam81)! + + +[2023-12-01] Version 6.11.0 +--------------------------- +**Library - Feature** +- [PR #1073](https://github.com/sendgrid/sendgrid-python/pull/1073): geolocation setter in sendgrid-python for GDPR compliance. Thanks to [@manisha1997](https://github.com/manisha1997)! + +**Library - Test** +- [PR #1066](https://github.com/sendgrid/sendgrid-python/pull/1066): removing codedev test dependency. Thanks to [@sethgrid](https://github.com/sethgrid)! + + +[2023-03-22] Version 6.10.0 +--------------------------- +**Library - Feature** +- [PR #1062](https://github.com/sendgrid/sendgrid-python/pull/1062): Add reply_to_list functionality. Thanks to [@thepuzzlemaster](https://github.com/thepuzzlemaster)! +- [PR #1059](https://github.com/sendgrid/sendgrid-python/pull/1059): Add Python 3.11 to the testing. Thanks to [@cclauss](https://github.com/cclauss)! + +**Library - Miscellaneous** +- [PR #1065](https://github.com/sendgrid/sendgrid-python/pull/1065): Create GitHub Action to lint Python code. Thanks to [@cclauss](https://github.com/cclauss)! +- [PR #1064](https://github.com/sendgrid/sendgrid-python/pull/1064): Upgrade GitHub Action test-and-deploy.yml. Thanks to [@cclauss](https://github.com/cclauss)! +- [PR #1063](https://github.com/sendgrid/sendgrid-python/pull/1063): Upgrade GitHub Action pr-lint.yml. Thanks to [@cclauss](https://github.com/cclauss)! + +**Library - Test** +- [PR #1058](https://github.com/sendgrid/sendgrid-python/pull/1058): Adding misc as PR type. Thanks to [@rakatyal](https://github.com/rakatyal)! + +**Library - Docs** +- [PR #1055](https://github.com/sendgrid/sendgrid-python/pull/1055): Modify README.md in alignment with SendGrid Support. Thanks to [@garethpaul](https://github.com/garethpaul)! +- [PR #1052](https://github.com/sendgrid/sendgrid-python/pull/1052): Fix link that has drifted. Thanks to [@jonathanberger](https://github.com/jonathanberger)! + + +[2022-03-09] Version 6.9.7 +-------------------------- +**Library - Chore** +- [PR #1048](https://github.com/sendgrid/sendgrid-python/pull/1048): Update mail_example.py. Thanks to [@vmesel](https://github.com/vmesel)! +- [PR #1049](https://github.com/sendgrid/sendgrid-python/pull/1049): push Datadog Release Metric upon deploy success. Thanks to [@eshanholtz](https://github.com/eshanholtz)! +- [PR #1050](https://github.com/sendgrid/sendgrid-python/pull/1050): fix flask dependency test issues. Thanks to [@eshanholtz](https://github.com/eshanholtz)! + + +[2022-02-09] Version 6.9.6 +-------------------------- +**Library - Chore** +- [PR #1044](https://github.com/sendgrid/sendgrid-python/pull/1044): drop pytest which was not being used. Thanks to [@childish-sambino](https://github.com/childish-sambino)! +- [PR #1043](https://github.com/sendgrid/sendgrid-python/pull/1043): upgrade supported language versions. Thanks to [@childish-sambino](https://github.com/childish-sambino)! +- [PR #1041](https://github.com/sendgrid/sendgrid-python/pull/1041): add gh release to workflow. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! +- [PR #1039](https://github.com/sendgrid/sendgrid-python/pull/1039): merge test and deploy workflows. Thanks to [@Hunga1](https://github.com/Hunga1)! + + +[2022-01-26] Version 6.9.5 +-------------------------- +**Library - Docs** +- [PR #1036](https://github.com/sendgrid/sendgrid-python/pull/1036): Removing unused json import. Thanks to [@vital101](https://github.com/vital101)! + + +[2022-01-12] Version 6.9.4 +-------------------------- +**Library - Chore** +- [PR #1031](https://github.com/sendgrid/sendgrid-python/pull/1031): Remove unused import from distutils. Thanks to [@tirkarthi](https://github.com/tirkarthi)! + +**Library - Docs** +- [PR #1032](https://github.com/sendgrid/sendgrid-python/pull/1032): remove leading spaces on error handling example. Thanks to [@thinkingserious](https://github.com/thinkingserious)! + + +[2021-12-15] Version 6.9.3 +-------------------------- +**Library - Test** +- [PR #1029](https://github.com/sendgrid/sendgrid-python/pull/1029): split up unit and integ tests. Thanks to [@childish-sambino](https://github.com/childish-sambino)! + + +[2021-12-01] Version 6.9.2 +-------------------------- +**Library - Chore** +- [PR #1027](https://github.com/sendgrid/sendgrid-python/pull/1027): migrate to GitHub Actions. Thanks to [@JenniferMah](https://github.com/JenniferMah)! + + +[2021-11-17] Version 6.9.1 +-------------------------- +**Library - Chore** +- [PR #1022](https://github.com/sendgrid/sendgrid-python/pull/1022): fix vulnerability in starbank-ecdsa dependency. Thanks to [@hellno](https://github.com/hellno)! + + +[2021-11-03] Version 6.9.0 +-------------------------- +**Library - Feature** +- [PR #1020](https://github.com/sendgrid/sendgrid-python/pull/1020): allow personalization of the From name and email for each recipient. Thanks to [@beebzz](https://github.com/beebzz)! + + +[2021-10-18] Version 6.8.3 +-------------------------- +**Library - Chore** +- [PR #1016](https://github.com/sendgrid/sendgrid-python/pull/1016): pin starkbank-ecdsa version. Thanks to [@eshanholtz](https://github.com/eshanholtz)! +- [PR #1015](https://github.com/sendgrid/sendgrid-python/pull/1015): pin starkbank-ecdsa version. Thanks to [@eshanholtz](https://github.com/eshanholtz)! + +**Library - Docs** +- [PR #1013](https://github.com/sendgrid/sendgrid-python/pull/1013): improve signed event webhook validation docs. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + +[2021-09-22] Version 6.8.2 +-------------------------- +**Library - Chore** +- [PR #1007](https://github.com/sendgrid/sendgrid-python/pull/1007): test against v3.9. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + +[2021-08-25] Version 6.8.1 +-------------------------- +**Library - Chore** +- [PR #1003](https://github.com/sendgrid/sendgrid-python/pull/1003): get rid of reply_to in mail helper. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + +[2021-08-11] Version 6.8.0 +-------------------------- +**Library - Feature** +- [PR #999](https://github.com/sendgrid/sendgrid-python/pull/999): add reply_to to helpers.Mail. Thanks to [@vindarel](https://github.com/vindarel)! + + +[2021-06-16] Version 6.7.1 +-------------------------- +**Library - Chore** +- [PR #994](https://github.com/sendgrid/sendgrid-python/pull/994): remove logic adding quotes to names containing , and ;. Thanks to [@JenniferMah](https://github.com/JenniferMah)! + + +[2021-04-21] Version 6.7.0 +-------------------------- +**Library - Docs** +- [PR #986](https://github.com/sendgrid/sendgrid-python/pull/986): Update to_emails type. Thanks to [@PyGeek03](https://github.com/PyGeek03)! + +**Library - Feature** +- [PR #983](https://github.com/sendgrid/sendgrid-python/pull/983): add v3 bypass filters. Thanks to [@anarayanan604](https://github.com/anarayanan604)! + + +[2021-02-10] Version 6.6.0 +-------------------------- +**Library - Docs** +- [PR #964](https://github.com/sendgrid/sendgrid-python/pull/964): Use correct pip installation command. Thanks to [@Akasurde](https://github.com/Akasurde)! + +**Library - Fix** +- [PR #971](https://github.com/sendgrid/sendgrid-python/pull/971): replace names in BatchId docstrings. Thanks to [@bennylope](https://github.com/bennylope)! + +**Library - Feature** +- [PR #924](https://github.com/sendgrid/sendgrid-python/pull/924): remove duplicate emails ignoring case in Personalization. Thanks to [@DougCal](https://github.com/DougCal)! + + +[2021-01-13] Version 6.5.0 +-------------------------- +**Library - Feature** +- [PR #945](https://github.com/sendgrid/sendgrid-python/pull/945): Support for AMP HTML Email. Thanks to [@modernwarfareuplink](https://github.com/modernwarfareuplink)! + +**Library - Docs** +- [PR #962](https://github.com/sendgrid/sendgrid-python/pull/962): Sending HTML email example is broken. Thanks to [@mikeckennedy](https://github.com/mikeckennedy)! + + +[2020-12-02] Version 6.4.8 +-------------------------- +**Library - Docs** +- [PR #955](https://github.com/sendgrid/sendgrid-python/pull/955): fixed typo in sendgrid/helpers/mail/file_content.py. Thanks to [@razvandimescu](https://github.com/razvandimescu)! + + +[2020-09-16] Version 6.4.7 +-------------------------- +**Library - Docs** +- [PR #936](https://github.com/sendgrid/sendgrid-python/pull/936): correct attachment example. Thanks to [@Arbitrage0](https://github.com/Arbitrage0)! + + +[2020-08-19] Version 6.4.6 +-------------------------- +**Library - Chore** +- [PR #929](https://github.com/sendgrid/sendgrid-python/pull/929): update GitHub branch references to use HEAD. Thanks to [@thinkingserious](https://github.com/thinkingserious)! + + [2020-08-05] Version 6.4.5 -------------------------- **Library - Docs** @@ -152,7 +363,7 @@ All notable changes to this project will be documented in this file. ### Added - Twilio SendGrid branding -- [Twilio SMS example](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/sms.md) +- [Twilio SMS example](use_cases/sms.md) - Updated CLA process ## [6.0.0] - 2019-04-02 ## @@ -162,7 +373,7 @@ All notable changes to this project will be documented in this file. - The `Mail` helper signature has changed. - Setting up a `SendGridAPIClient` has changed. -Please see the [use cases documentation](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md) for implemenation details. +Please see the [use cases documentation](use_cases/README.md) for implemenation details. This refactor was based on [this issue](https://github.com/sendgrid/sendgrid-python/issues/347). BIG thanks to all of those who [participated](https://github.com/sendgrid/sendgrid-python/issues/323) in shaping this release. @@ -198,7 +409,7 @@ In particular, BIG THANKS to: - [PR #488](https://github.com/sendgrid/sendgrid-python/pull/488): Fix similar code issue in mail.py helper (BIG thanks to [@adiman9](https://github.com/adiman9)) - [PR #496](https://github.com/sendgrid/sendgrid-python/pull/496): Fix issues in sendgrid/helpers/mail/mail.py (BIG thanks to [@galihmelon](https://github.com/galihmelon)) - [PR #510](https://github.com/sendgrid/sendgrid-python/pull/510): Fix similar code issue in sendgrid/helpers/mail/mail.py (BIG thanks to [@nanspro](https://github.com/nanspro)) -- [PR #524](https://github.com/sendgrid/sendgrid-python/pull/524): Fix master failure on travis (relating to ASM raise-assertion). (BIG thanks to [@extemporalgenome](https://github.com/extemporalgenome)) +- [PR #524](https://github.com/sendgrid/sendgrid-python/pull/524): Fix main failure on travis (relating to ASM raise-assertion). (BIG thanks to [@extemporalgenome](https://github.com/extemporalgenome)) ### Added - [PR #666](https://github.com/sendgrid/sendgrid-python/pull/666): Created First-timers.md File (BIG thanks to [@jaykay12](https://github.com/jaykay12)) @@ -316,7 +527,7 @@ Removed the trailing white spaces. Big thanks to [Siddaram Halli](https://github ## [5.0.0] - 2017-08-11 ### BREAKING CHANGE - The breaking change actually happened in [version 4.2.1](https://github.com/sendgrid/sendgrid-python/releases/tag/v4.2.1), where I mistakenly applied a patch version bump. See issues #328 and #321 for details. -- This version (5.0.0) replaces error handling via HTTPError from urllib in favor of custom error handling via the [HTTPError class](https://github.com/sendgrid/python-http-client/blob/master/python_http_client/exceptions.py) as was the case in version 4.2.0. +- This version (5.0.0) replaces error handling via HTTPError from urllib in favor of custom error handling via the [HTTPError class](https://github.com/sendgrid/python-http-client/blob/HEAD/python_http_client/exceptions.py) as was the case in version 4.2.0. ## [4.2.1] - 2017-08-03 ## ### Fixed @@ -337,7 +548,7 @@ Removed the trailing white spaces. Big thanks to [Siddaram Halli](https://github ### BREAKING CHANGE - Pull #244 [refactor helpers using property getter/setter](https://github.com/sendgrid/sendgrid-python/pull/244/files) - Big thanks to [Denis Vlasov](https://github.com/denis90) for the pull request! -- The changes break the implementation of the [Mail Helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail) `Mail()` class +- The changes break the implementation of the [Mail Helper](sendgrid/helpers/mail) `Mail()` class - `set_from()` is now the property `from_email` - `set_subject()` is now the property `subject` - `set_template_id()` is now the property `template_id` @@ -426,7 +637,7 @@ Removed the trailing white spaces. Big thanks to [Siddaram Halli](https://github ## [3.2.2] - 2016-08-23 ## ### Added - Table of Contents in the README -- Added a [USE_CASES.md](https://github.com/sendgrid/sendgrid-python/blob/master/USE_CASES.md) section, with the first use case example for transactional templates +- Added a [USE_CASES.md](USE_CASES.md) section, with the first use case example for transactional templates ## [3.2.1] - 2016-08-17 ## ### Fixed @@ -448,7 +659,7 @@ Removed the trailing white spaces. Big thanks to [Siddaram Halli](https://github ## [3.1.8] - 2016-07-25 ## ### Added -- [Troubleshooting](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md) section +- [Troubleshooting](TROUBLESHOOTING.md) section ## [3.1.7] - 2016-07-25 ## ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed57a5722..af9507154 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,6 @@ Hello! Thank you for choosing to help contribute to one of the Twilio SendGrid o All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. -- [Feature Request](#feature-request) -- [Submit a Bug Report](#submit-a-bug-report) - - [Please use our Bug Report Template](#please-use-our-bug-report-template) - [Improvements to the Codebase](#improvements-to-the-codebase) - [Development Environment](#development-environment) - [Prerequisites](#prerequisites) @@ -17,36 +14,8 @@ All third party contributors acknowledge that any contributions they provide wil - [Creating a Pull Request](#creating-a-pull-request) - [Code Reviews](#code-reviews) - -We use [Milestones](https://github.com/sendgrid/sendgrid-python/milestones) to help define current roadmaps, please feel free to grab an issue from the current milestone. Please indicate that you have begun work on it to avoid collisions. Once a PR is made, community reviews, comments, suggestions, and additional PRs are welcomed and encouraged. - There are a few ways to contribute, which we'll enumerate below: -## Feature Request - -If you'd like to make a feature request, please read this section. - -The GitHub issue tracker is the preferred channel for library feature requests, but please respect the following restrictions: - -- Please **search for existing issues** in order to ensure we don't have duplicate bugs/feature requests. -- Please be respectful and considerate of others when commenting on issues - -## Submit a Bug Report - -Note: DO NOT include your credentials in ANY code examples, descriptions, or media you make public. - -A software bug is a demonstrable issue in the code base. In order for us to diagnose the issue and respond as quickly as possible, please add as much detail as possible into your bug report. - -Before you decide to create a new issue, please try the following: - -1. Check the GitHub issues tab if the identified issue has already been reported, if so, please add a +1 to the existing post. -2. Update to the latest version of this code and check if the issue has already been fixed -3. Copy and fill in the Bug Report Template we have provided below - -### Please use our Bug Report Template - -In order to make the process easier, we've included a [sample bug report template](ISSUE_TEMPLATE.md). - ## Improvements to the Codebase We welcome direct contributions to the sendgrid-python code base. Thank you! @@ -57,7 +26,7 @@ We welcome direct contributions to the sendgrid-python code base. Thank you! - Python version 2.7, 3.5, 3.6, 3.7, or 3.8 - [python_http_client](https://github.com/sendgrid/python-http-client) -- [ecdsa_python](https://github.com/starkbank/ecdsa-python) +- [cryptography](https://github.com/pyca/cryptography) - [pyenv](https://github.com/yyuu/pyenv) - [tox](https://pypi.python.org/pypi/tox) @@ -87,7 +56,7 @@ source .env #### Execute -See the [examples folder](https://github.com/sendgrid/sendgrid-python/tree/master/examples) to get started quickly. +See the [examples folder](examples) to get started quickly. If testing from the root directory of this repo, create a new file (e.g. test.py) and replace `import sendgrid` with `from sendgrid import *` @@ -104,7 +73,7 @@ If testing from the root directory of this repo, create a new file (e.g. test.py The PR must pass all the tests before it is reviewed. -All test files are in the [`test`](https://github.com/sendgrid/sendgrid-python/test) directory. For the purposes of contributing to this repo, please update the [`test_sendgrid.py`](https://github.com/sendgrid/sendgrid-python/tree/master/test/test_sendgrid.py) file with unit tests as you modify the code. +All test files are in the [`test`](test) directory. For the purposes of contributing to this repo, please update the [`test_sendgrid.py`](test/test_sendgrid.py) file with unit tests as you modify the code. The integration tests require a Twilio SendGrid mock API in order to execute. We've simplified setting this up using Docker to run the tests. You will just need [Docker Desktop](https://docs.docker.com/get-docker/) and `make`. @@ -165,7 +134,7 @@ Please run your code through: 5. Locally merge (or rebase) the upstream development branch into your topic branch: ```bash - git pull [--rebase] upstream master + git pull [--rebase] upstream main ``` 6. Push your topic branch up to your fork: @@ -175,7 +144,7 @@ Please run your code through: ``` 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) - with a clear title and description against the `master` branch. All tests must be passing before we will review the PR. + with a clear title and description against the `main` branch. All tests must be passing before we will review the PR. ## Code Reviews If you can, please look at open PRs and review them. Give feedback and help us merge these PRs much faster! If you don't know how, GitHub has some great [information on how to review a Pull Request](https://help.github.com/articles/about-pull-request-reviews/). diff --git a/FIRST_TIMERS.md b/FIRST_TIMERS.md index b0636fb81..a1d563a75 100644 --- a/FIRST_TIMERS.md +++ b/FIRST_TIMERS.md @@ -1,69 +1,53 @@ -## Welcome to the Twilio SendGrid Open Source Community -If you are new to Open Source, you are at the right place to start with. Contributions are always encouraged & appreciated. Just follow the organisation's Contribution Policies & you are good to go. - -## How to get Started? -- [Explore Twilio SendGrid](#explore) -- [Raise Issues(If Found Any)](#issues) -- [Setting up the Development Environment](#setup) -- [Proposing Change through a Pull Request](#pr) -- [Be Patient & Wait for reviews](#reviews) - - -### Explore Twilio SendGrid -Step 1: Get yourself Access to Twilio SendGrid API Service absolutely free from [here](https://sendgrid.com/free/?source=sendgrid-python) \ -Step 2: Get familiar with Twilio SendGrid Service -- Prerequisites are Python version 2.6, 2.7, 3.4, 3.5 or 3.6 -- Set up your [Twilio SendGrid API Key](https://app.sendgrid.com/settings/api_keys) to your local workspace [using](https://github.com/sendgrid/sendgrid-python#setup-environment-variables) -- Install Twilio SendGrid to your workspace using `pip install sendgrid` -- Copy & Run few sample programs from [here](https://github.com/sendgrid/sendgrid-python#hello-email) - - -### Raise Issues -Twilio SendGrid uses GitHub as the content management service so, all the issues related to the project be it some feature request or a bug report, all are reported at the [GitHub Issue Tracker](https://github.com/sendgrid/sendgrid-python/issues)\ -Kindly make sure, to check for any duplicate issues raised by fellow contributors before opening a new issue. Be humble & polite while commenting on issues -- Feature Request\ - In case you feel like something is missing or lacking in the API Service, feel free to share your views & opinions with the community -- Bug Report\ - If you encounter any sort of bug or abnormal behavior, feel free to inform the community after performing the following checks: - - Update to the latest version & check if the bug persists - - Check the Issue Tracker for any similar bug report - - Finally, fill up the Bug Report Template & Open the Issue highlighting your encountered bug & detailed steps to regenerate the bug. - - -### Setting up the Development Environment -- **Setting up Locally** - - Step 1: Install the Prerequistes: Any Version of Python (2.6 through 3.6) and both [python_http_client](https://github.com/sendgrid/python-http-client) and [ecdsa_python](https://github.com/starkbank/ecdsa-python) - - Step 2: Get a local copy of repository using `git clone https://github.com/sendgrid/sendgrid-python.git` - - Step 3: Set your [Twilio SendGrid API Key](https://app.sendgrid.com/settings/api_keys) to your local workspace using\ - `echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env`\ - `echo "sendgrid.env" >> .gitignore`\ - `source ./sendgrid.env` - - Step 4: The entire codebase consist of 3 major divisions -- **/examples** contains *Working examples that demonstrate usage* -- **/tests** contains *the unit and profiling tests* -- **/sendgrid** contains *the Web API v3 client ie sendgrid.py and other files*. - - -## Proposing Change through a Pull Request -**Step 1:** Fork the project & Clone your fork using `git clone https://github.com//sendgrid-python.git` - -**Step 2:** Reconfigure the remotes using `cd sendgrid-python` and `git remote add upstream https://github.com/sendgrid/sendgrid-python.git` - -**Step 3:** Create a new branch for your modifications using `git checkout -b ` - -**Step 4:** Commit the changes in logical chunks & add commit messages strictly following [this](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - -**Step 5:** Run all test locally, [for more info](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#testing) - -**Step 6:** Locally merge your the upstream development branch into your topic-branch using `git pull [--rebase] upstream master` - -**Step 7:** Push the topic branch up to your fork using `git push origin ` - -**Step 8:** Open a Pull Request with clear title and description against the master branch. - - -## Be Patient & Wait for Reviews -Kindly be patient & follow the suggestions as provided by the peer reviewers. Make required amendments & changes to the PR as asked. - -## [Explore New Issues to work upon](https://github.com/sendgrid/sendgrid-python/labels/difficulty%3A%20easy) +# How To Contribute to Twilio SendGrid Repositories via GitHub +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. + +To make a pull request, follow these steps: + +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. + + + +2. __[Fork](https://help.github.com/fork-a-repo/)__ the [sendgrid-python](https://github.com/sendgrid/sendgrid-python) repository: + + + +3. __Clone__ your fork via the following commands: + +```bash +# Clone your fork of the repo into the current directory +git clone https://github.com/your_username/sendgrid-python +# Navigate to the newly cloned directory +cd sendgrid-python +# Assign the original repo to a remote called "upstream" +git remote add upstream https://github.com/sendgrid/sendgrid-python +``` + +> Don't forget to replace *your_username* in the URL by your real GitHub username. + +4. __Create a new topic branch__ (off the main project development branch) to contain your feature, change, or fix: + +```bash +git checkout -b +``` + +5. __Commit your changes__ in logical chunks. + +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. + +6. __Locally merge (or rebase)__ the upstream development branch into your topic branch: + +```bash +git pull [--rebase] upstream main +``` + +7. __Push__ your topic branch up to your fork: + +```bash +git push origin +``` + +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. + +## Important notice + +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. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index fb2e15cef..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ - - -### Issue Summary -A summary of the issue and the environment in which it occurs. If suitable, include the steps required to reproduce the bug. Please feel free to include screenshots, screencasts, or code examples. - -### Steps to Reproduce -1. This is the first step -2. This is the second step -3. Further steps, etc. - -### Code Snippet -```python -# paste code here -``` - -### Exception/Log -``` -# paste exception/log here -``` - -### Technical details: -* sendgrid-python version: -* python version: - diff --git a/LICENSE.md b/LICENSE similarity index 94% rename from LICENSE.md rename to LICENSE index 29aba592a..126ceb1a3 100644 --- a/LICENSE.md +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2020, Twilio SendGrid, Inc. +Copyright (C) 2025, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/MANIFEST.in b/MANIFEST.in index cd3c5f680..bf5b007ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst -include LICENSE.md +include LICENSE include app.json include Procfile include requirements.txt diff --git a/Makefile b/Makefile index 620a25993..96161106d 100644 --- a/Makefile +++ b/Makefile @@ -2,19 +2,19 @@ venv: clean @python --version || (echo "Python is not installed, please install Python 2 or Python 3"; exit 1); + pip install virtualenv virtualenv --python=python venv install: venv + . venv/bin/activate; pip install -r test/requirements.txt . venv/bin/activate; python setup.py install . venv/bin/activate; pip install -r requirements.txt -test-install: install - . venv/bin/activate; pip install -r test/requirements.txt - -test: test-install +test: install + . venv/bin/activate; coverage run -m unittest discover -s test/unit test-integ: test - . venv/bin/activate; coverage run -m unittest discover + . venv/bin/activate; coverage run -m unittest discover -s test/integ version ?= latest test-docker: diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a86818029..f9448a3b1 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ We appreciate the effort for this pull request but before that please make sure Please format the PR title appropriately based on the type of change: [!]: -Where is one of: docs, chore, feat, fix, test. +Where is one of: docs, chore, feat, fix, test, misc. Add a '!' after the type for breaking changes (e.g. feat!: new breaking feature). **All third-party contributors acknowledge that any contributions they provide will be made under the same open-source license that the open-source project is provided under.** @@ -19,13 +19,13 @@ Closes #2 A short description of what this PR does. ### Checklist -- [ ] I acknowledge that all my contributions will be made under the project's license +- [x] I acknowledge that all my contributions will be made under the project's license - [ ] I have made a material change to the repo (functionality, testing, spelling, grammar) -- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) and my PR follows them +- [ ] I have read the [Contribution Guidelines](https://github.com/sendgrid/sendgrid-python/blob/main/CONTRIBUTING.md) and my PR follows them - [ ] I have titled the PR appropriately - [ ] I have updated my branch with the main branch - [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have added necessary documentation about the functionality in the appropriate .md file +- [ ] I have added the necessary documentation about the functionality in the appropriate .md file - [ ] I have added inline documentation to the code I modified -If you have questions, please file a [support ticket](https://twilio.com/help/contact), or create a GitHub Issue in this repository. +If you have questions, please file a [support ticket](https://support.sendgrid.com). \ No newline at end of file diff --git a/README.md b/README.md index 9ac2fbc3b..b1b36686f 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,21 @@ -![SendGrid Logo](https://uiux.s3.amazonaws.com/2016-logos/email-logo%402x.png) +![SendGrid Logo](twilio_sendgrid_logo.png) -[![Travis Badge](https://travis-ci.org/sendgrid/sendgrid-python.svg?branch=master)](https://travis-ci.org/sendgrid/sendgrid-python) -[![codecov](https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/master.svg?style=flat-square&label=Codecov+Coverage)](https://codecov.io/gh/sendgrid/sendgrid-python) +[![BuildStatus](https://github.com/sendgrid/sendgrid-python/actions/workflows/test-and-deploy.yml/badge.svg)](https://github.com/sendgrid/sendgrid-python/actions/workflows/test-and-deploy.yml) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) -[![Email Notifications Badge](https://dx.sendgrid.com/badge/python)](https://dx.sendgrid.com/newsletter/python) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.md) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) [![GitHub contributors](https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg)](https://github.com/sendgrid/sendgrid-python/graphs/contributors) [![Open Source Helpers](https://www.codetriage.com/sendgrid/sendgrid-python/badges/users.svg)](https://www.codetriage.com/sendgrid/sendgrid-python) -**The default branch name for this repository has been changed to `main` as of 07/27/2020.** - **This library allows you to quickly and easily use the SendGrid Web API v3 via Python.** Version 3.X.X+ of this library provides full support for all SendGrid [Web API v3](https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html) endpoints, including the new [v3 /mail/send](https://sendgrid.com/blog/introducing-v3mailsend-sendgrids-new-mail-endpoint). -This library represents the beginning of a new path for SendGrid. We want this library to be community driven and SendGrid led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/sendgrid/sendgrid-python/issues) and [pull requests](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md) or simply upvote or comment on existing issues or pull requests. +This library represents the beginning of a new path for SendGrid. We want this library to be community driven and SendGrid led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/sendgrid/sendgrid-python/issues) and [pull requests](CONTRIBUTING.md) or simply upvote or comment on existing issues or pull requests. -Please browse the rest of this README for further detail. +**If you need help using SendGrid, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com).** -We appreciate your continued support, thank you! +Please browse the rest of this README for further detail. # Table of Contents @@ -29,10 +25,10 @@ We appreciate your continued support, thank you! * [Usage](#usage) * [Use Cases](#use-cases) * [Announcements](#announcements) -* [Roadmap](#roadmap) * [How to Contribute](#contribute) * [Troubleshooting](#troubleshooting) * [About](#about) +* [Support](#support) * [License](#license) @@ -41,7 +37,7 @@ We appreciate your continued support, thank you! ## Prerequisites -- Python version 2.7, 3.5, 3.6, 3.7, or 3.8 +- Python version 2.7+ - The SendGrid service, starting at the [free level](https://sendgrid.com/free?source=sendgrid-python) ## Setup Environment Variables @@ -74,7 +70,7 @@ pip install sendgrid ## Dependencies - [Python-HTTP-Client](https://github.com/sendgrid/python-http-client) -- [ECDSA-Python](https://github.com/starkbank/ecdsa-python) +- [Cryptography](https://github.com/pyca/cryptography) @@ -82,7 +78,7 @@ pip install sendgrid ## Hello Email -The following is the minimum needed code to send an email with the [/mail/send Helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail) ([here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/helpers/mail_example.py#L9) is a full example): +The following is the minimum needed code to send an email with the [/mail/send Helper](sendgrid/helpers/mail) ([here](examples/helpers/mail_example.py#L9) is a full example): ### With Mail Helper Class @@ -103,11 +99,11 @@ print(response.body) print(response.headers) ``` -The `Mail` constructor creates a [personalization object](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html) for you. [Here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/helpers/mail_example.py#L16) is an example of how to add it. +The `Mail` constructor creates a [personalization object](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html) for you. [Here](examples/helpers/mail_example.py#L28) is an example of how to add it. ### Without Mail Helper Class -The following is the minimum needed code to send an email without the /mail/send Helper ([here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/mail/mail.py#L27) is a full example): +The following is the minimum needed code to send an email without the /mail/send Helper ([here](examples/mail/mail.py#L27) is a full example): ```python import sendgrid @@ -170,61 +166,55 @@ print(response.headers) # Processing Inbound Email -Please see [our helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound) for utilizing our Inbound Parse webhook. +Please see [our helper](sendgrid/helpers/inbound) for utilizing our Inbound Parse webhook. # Usage - [SendGrid Documentation](https://sendgrid.com/docs/API_Reference/index.html) -- [Library Usage Documentation](https://github.com/sendgrid/sendgrid-python/tree/master/USAGE.md) -- [Example Code](https://github.com/sendgrid/sendgrid-python/tree/master/examples) +- [Library Usage Documentation](USAGE.md) +- [Example Code](examples) - [How-to: Migration from v2 to v3](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/how_to_migrate_from_v2_to_v3_mail_send.html) -- [v3 Web API Mail Send Helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail) - build a request object payload for a v3 /mail/send API call. -- [Processing Inbound Email](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound) +- [v3 Web API Mail Send Helper](sendgrid/helpers/mail) - build a request object payload for a v3 /mail/send API call. +- [Processing Inbound Email](sendgrid/helpers/inbound) # Use Cases -[Examples of common API use cases](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md), such as how to send an email with a transactional template. +[Examples of common API use cases](use_cases/README.md), such as how to send an email with a transactional template. # Announcements -Please see our announcement regarding [breaking changes](https://github.com/sendgrid/sendgrid-python/issues/217). Your support is appreciated! - -All updates to this library are documented in our [CHANGELOG](https://github.com/sendgrid/sendgrid-python/blob/master/CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). You may also subscribe to email [release notifications](https://dx.sendgrid.com/newsletter/java) for releases and breaking changes. - - -# Roadmap - -If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/sendgrid/sendgrid-python/issues) and [pull requests](https://github.com/sendgrid/sendgrid-python/pulls). We would love to hear your feedback. +All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). # How to Contribute -We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md) guide for details. +We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](CONTRIBUTING.md) guide for details. Quick links: -- [Feature Request](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#feature-request) -- [Bug Reports](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#submit-a-bug-report) -- [Improvements to the Codebase](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#improvements-to-the-codebase) -- [Review Pull Requests](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#code-reviews) +- [Feature Request](CONTRIBUTING.md#feature-request) +- [Bug Reports](CONTRIBUTING.md#submit-a-bug-report) +- [Improvements to the Codebase](CONTRIBUTING.md#improvements-to-the-codebase) +- [Review Pull Requests](CONTRIBUTING.md#code-reviews) # Troubleshooting -Please see our [troubleshooting guide](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md) for common library issues. +Please see our [troubleshooting guide](TROUBLESHOOTING.md) for common library issues. # About sendgrid-python is maintained and funded by Twilio SendGrid, Inc. The names and logos for sendgrid-python are trademarks of Twilio SendGrid, Inc. -If you need help installing or using the library, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com). + +# Support -If you've instead found a bug in the library or would like new features added, go ahead and open issues or pull requests against this repo! +If you need support, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com). # License -[The MIT License (MIT)](LICENSE.md) +[The MIT License (MIT)](LICENSE) diff --git a/README.rst b/README.rst index 3ef04e638..526c4ca40 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,14 @@ -.. image:: https://github.com/sendgrid/sendgrid-python/raw/master/twilio_sendgrid_logo.png +.. image:: https://github.com/sendgrid/sendgrid-python/raw/HEAD/twilio_sendgrid_logo.png :target: https://www.sendgrid.com -|Travis Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |Email Notifications Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| +|Tests Badge| |Python Versions| |PyPI Version| |Docker Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| **This library allows you to quickly and easily use the Twilio SendGrid Web API v3 via Python.** **NEW:** -**The default branch name for this repository has been changed to `main` as of 07/27/2020.** - -- Subscribe to email `notifications`_ for releases and breaking changes. - Version 6.X release is a BREAKING CHANGE from version 5.X, please see the `release notes`_ for details. - Send SMS messages with `Twilio`_. @@ -35,7 +32,6 @@ Table of Contents - `General Usage <#usage>`__ - `Processing Inbound Email <#processing-inbound-email>`__ - `Announcements <#announcements>`__ -- `Roadmap <#roadmap>`__ - `How to Contribute <#how-to-contribute>`__ - `Troubleshooting <#troubleshooting>`__ - `About <#about>`__ @@ -94,7 +90,7 @@ Dependencies ------------ - `Python-HTTP-Client`_ -- `ECDSA-Python`_ +- `Cryptography`_ Quick Start =========== @@ -103,7 +99,7 @@ Hello Email ----------- The following is the minimum needed code to send an email with the `/mail/send Helper`_ -(`here `__ is a full example): +(`here `__ is a full example): With Mail Helper Class ~~~~~~~~~~~~~~~~~~~~~~ @@ -129,13 +125,13 @@ With Mail Helper Class print(str(e)) The ``Mail`` constructor creates a `personalization object`_ for you. -`Here `__ is an example of how to add it. +`Here `__ is an example of how to add it. Without Mail Helper Class ~~~~~~~~~~~~~~~~~~~~~~~~~ The following is the minimum needed code to send an email without the /mail/send Helper -(`here `__ is a full example): +(`here `__ is a full example): .. code:: python @@ -224,14 +220,6 @@ Announcements ============= All updates to this library are documented in our `CHANGELOG`_ and `releases`_. -You may also subscribe to email `release notifications`_ for releases and breaking changes. - -Roadmap -======= - -If you are interested in the future direction of this project, -please take a look at our open `issues`_ and `pull requests `__. -We would love to hear your feedback. How to Contribute ================= @@ -261,55 +249,49 @@ License `The MIT License (MIT)`_ -.. _notifications: https://dx.sendgrid.com/newsletter/python -.. _Twilio: https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/sms.md +.. _Twilio: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/sms.md .. _release notes: https://github.com/sendgrid/sendgrid-python/releases/tag/v6.0.0 .. _Web API v3: https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html .. _v3 /mail/send: https://sendgrid.com/blog/introducing-v3mailsend-sendgrids-new-mail-endpoint .. _issues: https://github.com/sendgrid/sendgrid-python/issues -.. _pull requests: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md +.. _pull requests: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md .. _free level: https://sendgrid.com/free?source=sendgrid-python .. _Twilio account: https://www.twilio.com/try-twilio?source=sendgrid-python .. _SENDGRID_API_KEY: https://app.sendgrid.com/settings/api_keys .. _Python-HTTP-Client: https://github.com/sendgrid/python-http-client -.. _ECDSA-Python: https://github.com/starkbank/ecdsa-python -.. _/mail/send Helper: https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail +.. _Cryptography: https://github.com/pyca/cryptography +.. _/mail/send Helper: https://github.com/sendgrid/sendgrid-python/tree/HEAD/sendgrid/helpers/mail .. _personalization object: https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html .. _Fluent Interface: https://sendgrid.com/blog/using-python-to-implement-a-fluent-interface-to-any-rest-api/ -.. _our helper: https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound +.. _our helper: https://github.com/sendgrid/sendgrid-python/tree/HEAD/sendgrid/helpers/inbound .. _Twilio SendGrid Documentation: https://sendgrid.com/docs/API_Reference/index.html -.. _Library Usage Documentation: https://github.com/sendgrid/sendgrid-python/tree/master/USAGE.md -.. _Example Code: https://github.com/sendgrid/sendgrid-python/tree/master/examples +.. _Library Usage Documentation: https://github.com/sendgrid/sendgrid-python/tree/HEAD/USAGE.md +.. _Example Code: https://github.com/sendgrid/sendgrid-python/tree/HEAD/examples .. _`How-to: Migration from v2 to v3`: https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/how_to_migrate_from_v2_to_v3_mail_send.html -.. _v3 Web API Mail Send Helper: https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail -.. _Processing Inbound Email: https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound -.. _Examples of common API use cases: https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md +.. _v3 Web API Mail Send Helper: https://github.com/sendgrid/sendgrid-python/tree/HEAD/sendgrid/helpers/mail +.. _Processing Inbound Email: https://github.com/sendgrid/sendgrid-python/tree/HEAD/sendgrid/helpers/inbound +.. _Examples of common API use cases: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/README.md .. _breaking changes: https://github.com/sendgrid/sendgrid-python/issues/217 -.. _CHANGELOG: https://github.com/sendgrid/sendgrid-python/blob/master/CHANGELOG.md +.. _CHANGELOG: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CHANGELOG.md .. _releases: https://github.com/sendgrid/sendgrid-python/releases -.. _release notifications: https://dx.sendgrid.com/newsletter/python -.. _CONTRIBUTING: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md -.. _Feature Request: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#feature-request -.. _Bug Reports: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#submit-a-bug-report -.. _Improvements to the Codebase: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#improvements-to-the-codebase -.. _Review Pull Requests: https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#code-reviews -.. _troubleshooting guide: https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md -.. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/master/LICENSE.md - -.. |Travis Badge| image:: https://travis-ci.org/sendgrid/sendgrid-python.svg?branch=master - :target: https://travis-ci.org/sendgrid/sendgrid-python +.. _CONTRIBUTING: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md +.. _Feature Request: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#feature-request +.. _Bug Reports: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#submit-a-bug-report +.. _Improvements to the Codebase: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#improvements-to-the-codebase +.. _Review Pull Requests: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#code-reviews +.. _troubleshooting guide: https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md +.. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/HEAD/LICENSE + +.. |Tests Badge| image:: https://github.com/sendgrid/sendgrid-python/actions/workflows/test.yml/badge.svg + :target: https://github.com/sendgrid/sendgrid-python/actions/workflows/test.yml .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/sendgrid.svg :target: https://pypi.org/project/sendgrid/ .. |PyPI Version| image:: https://img.shields.io/pypi/v/sendgrid.svg :target: https://pypi.org/project/sendgrid/ -.. |codecov| image:: https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/master.svg?style=flat-square&label=Codecov+Coverage - :target: https://codecov.io/gh/sendgrid/sendgrid-python .. |Docker Badge| image:: https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg :target: https://hub.docker.com/r/sendgrid/sendgrid-python/ -.. |Email Notifications Badge| image:: https://dx.sendgrid.com/badge/python - :target: https://dx.sendgrid.com/newsletter/python .. |MIT licensed| image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: ./LICENSE.md + :target: ./LICENSE .. |Twitter Follow| image:: https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow :target: https://twitter.com/sendgrid .. |GitHub contributors| image:: https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index f3a64ec3c..0a7f54c69 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -15,6 +15,7 @@ If you can't find a solution below, please open an [issue](https://github.com/se * [Version Convention](#versions) * [Viewing the Request Body](#request-body) * [Error Handling](#error-handling) +* [Verifying Event Webhooks](#signed-webhooks) ## Environment Variables and Your Twilio SendGrid API Key @@ -34,7 +35,7 @@ In the first case, SENDGRID_API_KEY is in reference to the name of the environme ## Error Messages -HTTP exceptions are defined in the [`python_http_client` package](https://github.com/sendgrid/python-http-client/blob/master/python_http_client/exceptions.py). +HTTP exceptions are defined in the [`python_http_client` package](https://github.com/sendgrid/python-http-client/blob/HEAD/python_http_client/exceptions.py). To read the error message returned by SendGrid's API in Python 2.X: @@ -72,7 +73,7 @@ Using pip: ```bash pip uninstall sendgrid -pip install sendgrid=1.6.22 +pip install sendgrid==1.6.22 ``` Download: @@ -100,7 +101,7 @@ If you are using a [requirements file](https://pip.readthedocs.io/en/1.1/require ## Versioning Convention -We follow the MAJOR.MINOR.PATCH versioning scheme as described by [SemVer.org](http://semver.org). Therefore, we recommend that you always pin (or vendor) the particular version you are working with to your code and never auto-update to the latest version. Especially when there is a MAJOR point release, since that is guaranteed to be a breaking change. Changes are documented in the [CHANGELOG](https://github.com/sendgrid/sendgrid-python/blob/master/CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases) section. +We follow the MAJOR.MINOR.PATCH versioning scheme as described by [SemVer.org](http://semver.org). Therefore, we recommend that you always pin (or vendor) the particular version you are working with to your code and never auto-update to the latest version. Especially when there is a MAJOR point release, since that is guaranteed to be a breaking change. Changes are documented in the [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases) section. ## Viewing the Request Body @@ -116,4 +117,15 @@ You can do this right before you call `response = sg.client.mail.send.post(reque # Error Handling -Please review [our use_cases](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md) for examples of error handling. +Please review [our use_cases](use_cases/README.md) for examples of error handling. + + +## Signed Webhook Verification + +Twilio SendGrid's Event Webhook will notify a URL via HTTP POST with information about events that occur as your mail is processed. [This](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features) article covers all you need to know to secure the Event Webhook, allowing you to verify that incoming requests originate from Twilio SendGrid. The sendgrid-python library can help you verify these Signed Event Webhooks. + +You can find the usage example [here](examples/helpers/eventwebhook/eventwebhook_example.py) and the tests [here](test/test_eventwebhook.py). +If you are still having trouble getting the validation to work, follow the following instructions: +- Be sure to use the *raw* payload for validation +- Be sure to include a trailing carriage return and newline in your payload +- In case of multi-event webhooks, make sure you include the trailing newline and carriage return after *each* event diff --git a/USAGE.md b/USAGE.md index b6d044676..978b675ab 100644 --- a/USAGE.md +++ b/USAGE.md @@ -2105,7 +2105,7 @@ For more detailed information about how to use the v3 Mail Send endpoint, please ### POST /mail/send -This endpoint has a helper, check it out [here](https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/mail/README.md). +This endpoint has a helper, check it out [here](sendgrid/helpers/mail/README.md). ```python data = { diff --git a/app.json b/app.json index b63428351..e25064d12 100644 --- a/app.json +++ b/app.json @@ -6,6 +6,6 @@ "inbound parse" ], "website": "http://www.sendgrid.com", - "repository": "https://github.com/sendgrid/sendgrid-python/tree/master", + "repository": "https://github.com/sendgrid/sendgrid-python", "logo": "https://sendgrid.com/brand/sg-twilio/SG_Twilio_Lockup_RGBx1.png" } \ No newline at end of file diff --git a/examples/dataresidency/set_region.py b/examples/dataresidency/set_region.py new file mode 100644 index 000000000..9aae2611f --- /dev/null +++ b/examples/dataresidency/set_region.py @@ -0,0 +1,37 @@ +import sendgrid +import os + +from sendgrid import Email, To, Content, Mail + +# Example 1 +# setting region to be "global" + +sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) +from_email = Email("example@abc.com") +to_email = To("example@abc.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/plain", "and easy to do anywhere, even with Python") +mail = Mail(from_email, to_email, subject, content) +sg.set_sendgrid_data_residency("global") +print(sg.client.host) +response = sg.client.mail.send.post(request_body=mail.get()) +print(response) +print(response.status_code) +print(response.body) +print(response.headers) + +# Example 2 +# setting region to "eu" +sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY_EU')) +sg.set_sendgrid_data_residency("eu") +from_email = Email("example@abc.com") +to_email = To("example@abc.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/plain", "and easy to do anywhere, even with Python") +print(sg.client.host) +mail = Mail(from_email, to_email, subject, content) +response = sg.client.mail.send.post(request_body=mail.get()) +print(response) +print(response.status_code) +print(response.body) +print(response.headers) \ No newline at end of file diff --git a/examples/helpers/README.md b/examples/helpers/README.md index 2eebbe421..8d7594d44 100644 --- a/examples/helpers/README.md +++ b/examples/helpers/README.md @@ -24,16 +24,20 @@ The `Content` class takes mainly two parameters: MIME type and the actual conten mail = Mail(from_email, to_email, subject, content) ``` After adding the above we create a mail object using `Mail` class, it takes the following parameters: email address to send from, subject line of emails, email address to send to, content of the message. -For more information on parameters and usage, see [here](https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/mail/mail.py) +For more information on parameters and usage, see [here](../mail/mail.py) ### Creating Personalizations -To create personalizations, you need a dictionary to store all your email components. See example [here](https://github.com/sendgrid/sendgrid-python/blob/0b683169b08d3a7c204107cd333be33053297e74/examples/helpers/mail_example.py#L47) -After creating a dictionary, you can go ahead and create a `Personalization` object. +The personalization helper can be used to create personalizations and customize various aspects of an email. See example [here](mail_example.py) in `build_multiple_emails_personalized()`, and refer [here](https://docs.sendgrid.com/for-developers/sending-email/personalizations) for more documentation. ``` mock_personalization = Personalization() - for to_addr in personalization['to_list']: - mock_personalization.add_to(to_addr) + + for to_addr in personalization['to_list']: + mock_personalization.add_to(to_addr) + + mock_personalization.set_from(from_addr) + mock_personalization.add_cc(cc_addr) + # etc... ``` ### Creating Attachments @@ -44,13 +48,13 @@ To create attachments, we use the `Attachment` class and make sure the content i attachment.content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" "Y3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQ3JhcyBwdW12") ``` -Another example: [Link](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/attachment.md) +Another example: [Link](../../use_cases/attachment.md) ### Managing Settings -To configure settings in mail, you can use the `MailSettings` class. The class takes some [parameters](https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/mail/mail_settings.py#L1)(such as bcc_settings, bypass_list_management, footer_settings, sandbox_mode) +To configure settings in mail, you can use the `MailSettings` class. The class takes some [parameters](../mailsettings/mailsettings.py#L1)(such as bcc_settings, bypass_list_management, footer_settings, sandbox_mode) -To add tracking settings, you can add `TrackingSettings` class. See example [here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/helpers/mail_example.py#L118) and parameters and usage [here](https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/mail/tracking_settings.py). +To add tracking settings, you can add `TrackingSettings` class. See example [here](mail_example.py#L118) and parameters and usage [here](../trackingsettings/trackingsettings.py). ### Sending email @@ -60,7 +64,7 @@ After you have configured every component and added your own functions, you can data = build_kitchen_sink() response = sg.send(data) ``` -Make sure you have [environment variable](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key) set up! +Make sure you have [environment variable](../../TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key) set up! Full example [here](https://github.com/sendgrid/sendgrid-python/blob/0b683169b08d3a7c204107cd333be33053297e74/examples/helpers/mail_example.py#L203). ### Using Dynamic Templates diff --git a/examples/helpers/mail_example.py b/examples/helpers/mail_example.py index c57520a9e..f6905787b 100644 --- a/examples/helpers/mail_example.py +++ b/examples/helpers/mail_example.py @@ -1,3 +1,6 @@ +import os +import json + from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import * @@ -8,12 +11,8 @@ def build_hello_email(): ## Send a Single Email to a Single Recipient - import os - import json - from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException - - message = Mail(from_email=From('from@example.com.com', 'Example From Name'), + + message = Mail(from_email=From('from@example.com', 'Example From Name'), to_emails=To('to@example.com', 'Example To Name'), subject=Subject('Sending with SendGrid is Fun'), plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), @@ -26,25 +25,30 @@ def build_hello_email(): except SendGridException as e: print(e.message) - for cc_addr in personalization['cc_list']: + mock_personalization = Personalization() + personalization_dict = get_mock_personalization_dict() + + for cc_addr in personalization_dict['cc_list']: mock_personalization.add_to(cc_addr) - for bcc_addr in personalization['bcc_list']: + for bcc_addr in personalization_dict['bcc_list']: mock_personalization.add_bcc(bcc_addr) - for header in personalization['headers']: + for header in personalization_dict['headers']: mock_personalization.add_header(header) - for substitution in personalization['substitutions']: + for substitution in personalization_dict['substitutions']: mock_personalization.add_substitution(substitution) - for arg in personalization['custom_args']: + for arg in personalization_dict['custom_args']: mock_personalization.add_custom_arg(arg) - mock_personalization.subject = personalization['subject'] - mock_personalization.send_at = personalization['send_at'] - return mock_personalization + mock_personalization.subject = personalization_dict['subject'] + mock_personalization.send_at = personalization_dict['send_at'] + + message.add_personalization(mock_personalization) + return message def get_mock_personalization_dict(): """Get a dict of personalization mock.""" @@ -78,15 +82,43 @@ def get_mock_personalization_dict(): mock_pers['send_at'] = 1443636843 return mock_pers +def build_multiple_emails_personalized(): + # Note that the domain for all From email addresses must match + + message = Mail(from_email=From('from@example.com', 'Example From Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), + html_content=HtmlContent('and easy to do anywhere, even with Python')) + + mock_personalization = Personalization() + mock_personalization.add_to(To('test@example.com', 'Example User 1')) + mock_personalization.add_cc(Cc('test1@example.com', 'Example User 2')) + message.add_personalization(mock_personalization) + + mock_personalization_2 = Personalization() + mock_personalization_2.add_to(To('test2@example.com', 'Example User 3')) + mock_personalization_2.set_from(From('from@example.com', 'Example From Name 2')) + mock_personalization_2.add_bcc(Bcc('test3@example.com', 'Example User 4')) + message.add_personalization(mock_personalization_2) + + try: + print(json.dumps(message.get(), sort_keys=True, indent=4)) + return message.get() + + except SendGridException as e: + print(e.message) + + return message def build_attachment1(): """Build attachment mock. Make sure your content is base64 encoded before passing into attachment.content. - Another example: https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/attachment.md""" + Another example: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/attachment.md""" + attachment = Attachment() - attachment.content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" + attachment.file_content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" "Y3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQ3JhcyBwdW12") - attachment.type = "application/pdf" - attachment.filename = "balance_001.pdf" + attachment.file_type = "application/pdf" + attachment.file_name = "balance_001.pdf" attachment.disposition = "attachment" attachment.content_id = "Balance Sheet" return attachment @@ -95,9 +127,9 @@ def build_attachment1(): def build_attachment2(): """Build attachment mock.""" attachment = Attachment() - attachment.content = "BwdW" - attachment.type = "image/png" - attachment.filename = "banner.png" + attachment.file_content = "BwdW" + attachment.file_type = "image/png" + attachment.file_name = "banner.png" attachment.disposition = "inline" attachment.content_id = "Banner" return attachment @@ -227,19 +259,19 @@ def build_kitchen_sink(): ] message.attachment = Attachment(FileContent('base64 encoded content 1'), - FileType('application/pdf'), FileName('balance_001.pdf'), + FileType('application/pdf'), Disposition('attachment'), ContentId('Content ID 1')) message.attachment = [ Attachment(FileContent('base64 encoded content 2'), - FileType('image/png'), FileName('banner.png'), + FileType('image/png'), Disposition('inline'), ContentId('Content ID 2')), Attachment(FileContent('base64 encoded content 3'), - FileType('image/png'), FileName('banner2.png'), + FileType('image/png'), Disposition('inline'), ContentId('Content ID 3')) ] @@ -308,10 +340,19 @@ def build_kitchen_sink(): return message +def send_multiple_emails_personalized(): + # Assumes you set your environment variable: + # https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key + message = build_multiple_emails_personalized() + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message=message) + print(response.status_code) + print(response.body) + print(response.headers) def send_hello_email(): # Assumes you set your environment variable: - # https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key + # https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key message = build_hello_email() sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) response = sendgrid_client.send(message=message) @@ -322,7 +363,7 @@ def send_hello_email(): def send_kitchen_sink(): # Assumes you set your environment variable: - # https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key + # https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key message = build_kitchen_sink() sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) response = sendgrid_client.send(message=message) @@ -334,5 +375,8 @@ def send_kitchen_sink(): ## this will actually send an email # send_hello_email() +## this will send multiple emails +# send_multiple_emails_personalized() + ## this will only send an email if you set SandBox Mode to False # send_kitchen_sink() diff --git a/examples/helpers/stats/stats_example.py b/examples/helpers/stats/stats_example.py index ebe24f69f..f22baa5c4 100644 --- a/examples/helpers/stats/stats_example.py +++ b/examples/helpers/stats/stats_example.py @@ -6,7 +6,7 @@ # NOTE: you will need to move this file to the root directory of this project to execute properly. # Assumes you set your environment variable: -# See: https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key +# See: https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) diff --git a/examples/mail/mail.py b/examples/mail/mail.py index d59901d1d..d2ccc80f0 100644 --- a/examples/mail/mail.py +++ b/examples/mail/mail.py @@ -27,7 +27,7 @@ # v3 Mail Send # # POST /mail/send # # This endpoint has a helper, check it out -# [here](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md). +# [here](https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/README.md). data = { "asm": { diff --git a/proposals/mail-helper-refactor.md b/proposals/mail-helper-refactor.md index 91ecfe878..70798c262 100644 --- a/proposals/mail-helper-refactor.md +++ b/proposals/mail-helper-refactor.md @@ -2,7 +2,7 @@ # Send a Single Email to a Single Recipient -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. This is the minimum code needed to send an email. @@ -29,7 +29,7 @@ except SendGridException as e: # Send a Single Email to Multiple Recipients -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. ```python import os @@ -57,7 +57,7 @@ except Exception as e: # Send Multiple Emails to Multiple Recipients -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. ```python import os @@ -100,7 +100,7 @@ except Exception as e: # Kitchen Sink - an example with all settings used -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. ```python import os @@ -246,7 +246,7 @@ except Exception as e: # Attachments -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. ```python import os @@ -275,7 +275,7 @@ except Exception as e: # Transactional Templates -The following code assumes you are storing the API key in an [environment variable (recommended)](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. +The following code assumes you are storing the API key in an [environment variable (recommended)](../TROUBLESHOOTING.md#environment). If you don't have your key stored in an environment variable, you can assign it directly to `apikey` for testing purposes. For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html). Following is the template content we used for testing. diff --git a/requirements.txt b/requirements.txt index ff1ba3c35..ed2594a90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Flask==1.0.2 +Flask==3.1.0 PyYAML>=4.2b1 python-http-client>=3.2.1 -six==1.11.0 -pytest==3.8.2 -starkbank-ecdsa>=1.0.0 +six==1.17.0 +cryptography>=45.0.6 +more-itertools==5.0.0 diff --git a/sendgrid/base_interface.py b/sendgrid/base_interface.py index 92b38247e..f94f09479 100644 --- a/sendgrid/base_interface.py +++ b/sendgrid/base_interface.py @@ -1,5 +1,6 @@ import python_http_client +region_host_dict = {'eu':'https://api.eu.sendgrid.com','global':'https://api.sendgrid.com'} class BaseInterface(object): def __init__(self, auth, host, impersonate_subuser): @@ -22,10 +23,10 @@ def __init__(self, auth, host, impersonate_subuser): """ from . import __version__ self.auth = auth - self.host = host self.impersonate_subuser = impersonate_subuser self.version = __version__ self.useragent = 'sendgrid/{};python'.format(self.version) + self.host = host self.client = python_http_client.Client( host=self.host, @@ -60,3 +61,23 @@ def send(self, message): message = message.get() return self.client.mail.send.post(request_body=message) + + def set_sendgrid_data_residency(self, region): + """ + Client libraries contain setters for specifying region/edge. + This supports global and eu regions only. This set will likely expand in the future. + Global is the default residency (or region) + Global region means the message will be sent through https://api.sendgrid.com + EU region means the message will be sent through https://api.eu.sendgrid.com + :param region: string + :return: + """ + if region in region_host_dict.keys(): + self.host = region_host_dict[region] + if self._default_headers is not None: + self.client = python_http_client.Client( + host=self.host, + request_headers=self._default_headers, + version=3) + else: + raise ValueError("region can only be \"eu\" or \"global\"") diff --git a/sendgrid/helpers/eventwebhook/__init__.py b/sendgrid/helpers/eventwebhook/__init__.py index a44eb5b89..9d618bf3a 100644 --- a/sendgrid/helpers/eventwebhook/__init__.py +++ b/sendgrid/helpers/eventwebhook/__init__.py @@ -1,7 +1,8 @@ -from ellipticcurve.ecdsa import Ecdsa -from ellipticcurve.publicKey import PublicKey -from ellipticcurve.signature import Signature - +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import load_pem_public_key +import base64 from .eventwebhook_header import EventWebhookHeader class EventWebhook: @@ -20,14 +21,15 @@ def __init__(self, public_key=None): def convert_public_key_to_ecdsa(self, public_key): """ - Convert the public key string to a ECPublicKey. + Convert the public key string to an EllipticCurvePublicKey object. :param public_key: verification key under Mail Settings :type public_key string - :return: public key using the ECDSA algorithm - :rtype PublicKey + :return: An EllipticCurvePublicKey object using the ECDSA algorithm + :rtype cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey """ - return PublicKey.fromPem(public_key) + pem_key = "-----BEGIN PUBLIC KEY-----\n" + public_key + "\n-----END PUBLIC KEY-----" + return load_pem_public_key(pem_key.encode("utf-8")) def verify_signature(self, payload, signature, timestamp, public_key=None): """ @@ -40,11 +42,15 @@ def verify_signature(self, payload, signature, timestamp, public_key=None): :param timestamp: value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header :type timestamp: string :param public_key: elliptic curve public key - :type public_key: PublicKey + :type public_key: cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey :return: true or false if signature is valid """ - timestamped_payload = timestamp + payload - decoded_signature = Signature.fromBase64(signature) + timestamped_payload = (timestamp + payload).encode('utf-8') + decoded_signature = base64.b64decode(signature) key = public_key or self.public_key - return Ecdsa.verify(timestamped_payload, decoded_signature, key) + try: + key.verify(decoded_signature, timestamped_payload, ec.ECDSA(hashes.SHA256())) + return True + except InvalidSignature: + return False diff --git a/sendgrid/helpers/inbound/README.md b/sendgrid/helpers/inbound/README.md index 93d0817b6..79e5b4544 100644 --- a/sendgrid/helpers/inbound/README.md +++ b/sendgrid/helpers/inbound/README.md @@ -36,7 +36,7 @@ pip install -r requirements.txt python sendgrid/helpers/inbound/send.py ./sendgrid/helpers/inbound/sample_data/default_data.txt ``` -More sample data can be found [here](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound/sample_data). +More sample data can be found [here](sample_data). View the results in the first terminal. @@ -71,7 +71,7 @@ Next, send an email to [anything]@inbound.yourdomain.com, then look at the termi Get a [Heroku](https://www.heroku.com) account. -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/sendgrid/sendgrid-python/tree/master) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/sendgrid/sendgrid-python/tree/main) [Setup your MX records.](https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html#-Setup) Depending on your domain name host, you may need to wait up to 48 hours for the settings to propagate. @@ -100,7 +100,7 @@ heroku git:remote -a [name-of-your-app] ---make changes--- git add . git commit -m "update configuration" -git push heroku master +git push heroku main ``` @@ -127,12 +127,12 @@ This module is used to send sample test data. It is useful for testing and devel Tests are located in the root of this project in the /test folder: -- [test_config.py](https://github.com/sendgrid/sendgrid-python/blob/master/test/test_config.py) -- [test_parse.py](https://github.com/sendgrid/sendgrid-python/blob/master/test/test_parse.py) +- [test_config.py](../../../test/test_config.py) +- [test_parse.py](../../../test/test_parse.py) -Learn about testing this code [here](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#testing). +Learn about testing this code [here](../../../CONTRIBUTING.md#testing). # Contributing -If you would like to contribute to this project, please see our [contributing guide](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md). Thanks! +If you would like to contribute to this project, please see our [contributing guide](../../../CONTRIBUTING.md). Thanks! diff --git a/sendgrid/helpers/inbound/templates/index.html b/sendgrid/helpers/inbound/templates/index.html index 0de3f44f3..7cbede381 100644 --- a/sendgrid/helpers/inbound/templates/index.html +++ b/sendgrid/helpers/inbound/templates/index.html @@ -5,6 +5,6 @@

You have successfully launched the server!

- Check out the documentation on how to use this software to utilize the SendGrid Inbound Parse webhook. + Check out the documentation on how to use this software to utilize the SendGrid Inbound Parse webhook. \ No newline at end of file diff --git a/sendgrid/helpers/mail/README.md b/sendgrid/helpers/mail/README.md index 29e21dea5..bbf0a2ece 100644 --- a/sendgrid/helpers/mail/README.md +++ b/sendgrid/helpers/mail/README.md @@ -6,5 +6,5 @@ Please complete the [installation steps](https://github.com/sendgrid/sendgrid-py ## Usage -- For the most common use cases, please see [these examples](https://github.com/sendgrid/sendgrid-python/tree/master/use_cases) +- For the most common use cases, please see [these examples](../../../use_cases) - The complete v3 API Documentation can be found [here](https://sendgrid.com/docs/API_Reference/api_v3.html) diff --git a/sendgrid/helpers/mail/__init__.py b/sendgrid/helpers/mail/__init__.py index 15cc1cc7e..358f2d912 100644 --- a/sendgrid/helpers/mail/__init__.py +++ b/sendgrid/helpers/mail/__init__.py @@ -4,7 +4,10 @@ from .bcc_email import Bcc from .bcc_settings import BccSettings from .bcc_settings_email import BccSettingsEmail +from .bypass_bounce_management import BypassBounceManagement from .bypass_list_management import BypassListManagement +from .bypass_spam_management import BypassSpamManagement +from .bypass_unsubscribe_management import BypassUnsubscribeManagement from .category import Category from .cc_email import Cc from .click_tracking import ClickTracking @@ -27,6 +30,7 @@ from .groups_to_display import GroupsToDisplay from .header import Header from .html_content import HtmlContent +from .amp_html_content import AmpHtmlContent from .ip_pool_name import IpPoolName from .mail_settings import MailSettings from .mail import Mail diff --git a/sendgrid/helpers/mail/amp_html_content.py b/sendgrid/helpers/mail/amp_html_content.py new file mode 100644 index 000000000..1a282053f --- /dev/null +++ b/sendgrid/helpers/mail/amp_html_content.py @@ -0,0 +1,59 @@ +from .content import Content +from .validators import ValidateApiKey + + +class AmpHtmlContent(Content): + """AMP HTML content to be included in your email.""" + + def __init__(self, content): + """Create an AMP HTML Content with the specified MIME type and content. + + :param content: The AMP HTML content. + :type content: string + """ + self._content = None + self._validator = ValidateApiKey() + + if content is not None: + self.content = content + + @property + def mime_type(self): + """The MIME type for AMP HTML content. + + :rtype: string + """ + return "text/x-amp-html" + + @property + def content(self): + """The actual AMP HTML content. + + :rtype: string + """ + return self._content + + @content.setter + def content(self, value): + """The actual AMP HTML content. + + :param value: The actual AMP HTML content. + :type value: string + """ + self._validator.validate_message_dict(value) + self._content = value + + def get(self): + """ + Get a JSON-ready representation of this AmpContent. + + :returns: This AmpContent, ready for use in a request body. + :rtype: dict + """ + content = {} + if self.mime_type is not None: + content["type"] = self.mime_type + + if self.content is not None: + content["value"] = self.content + return content diff --git a/sendgrid/helpers/mail/batch_id.py b/sendgrid/helpers/mail/batch_id.py index de9960ca2..a4c0f8e9d 100644 --- a/sendgrid/helpers/mail/batch_id.py +++ b/sendgrid/helpers/mail/batch_id.py @@ -18,7 +18,7 @@ def __init__(self, batch_id=None): @property def batch_id(self): - """A unix timestamp. + """The batch ID. :rtype: string """ @@ -26,7 +26,7 @@ def batch_id(self): @batch_id.setter def batch_id(self, value): - """A unix timestamp. + """The batch ID. :param value: Batch Id :type value: string @@ -42,7 +42,7 @@ def __str__(self): def get(self): """ - Get a JSON-ready representation of this SendAt object. + Get a JSON-ready representation of this BatchId object. :returns: The BatchId, ready for use in a request body. :rtype: string diff --git a/sendgrid/helpers/mail/bypass_bounce_management.py b/sendgrid/helpers/mail/bypass_bounce_management.py new file mode 100644 index 000000000..b0a35105c --- /dev/null +++ b/sendgrid/helpers/mail/bypass_bounce_management.py @@ -0,0 +1,48 @@ +class BypassBounceManagement(object): + """Setting for Bypass Bounce Management + + + Allows you to bypass the bounce list to ensure that the email is delivered to recipients. + Spam report and unsubscribe lists will still be checked; addresses on these other lists + will not receive the message. This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassBounceManagement. + + :param enable: Whether emails should bypass bounce management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassBounceManagement. + + :returns: This BypassBounceManagement, ready for use in a request body. + :rtype: dict + """ + bypass_bounce_management = {} + if self.enable is not None: + bypass_bounce_management["enable"] = self.enable + return bypass_bounce_management diff --git a/sendgrid/helpers/mail/bypass_spam_management.py b/sendgrid/helpers/mail/bypass_spam_management.py new file mode 100644 index 000000000..9b2552eb9 --- /dev/null +++ b/sendgrid/helpers/mail/bypass_spam_management.py @@ -0,0 +1,47 @@ +class BypassSpamManagement(object): + """Setting for Bypass Spam Management + + Allows you to bypass the spam report list to ensure that the email is delivered to recipients. + Bounce and unsubscribe lists will still be checked; addresses on these other lists will not + receive the message. This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassSpamManagement. + + :param enable: Whether emails should bypass spam management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassSpamManagement. + + :returns: This BypassSpamManagement, ready for use in a request body. + :rtype: dict + """ + bypass_spam_management = {} + if self.enable is not None: + bypass_spam_management["enable"] = self.enable + return bypass_spam_management diff --git a/sendgrid/helpers/mail/bypass_unsubscribe_management.py b/sendgrid/helpers/mail/bypass_unsubscribe_management.py new file mode 100644 index 000000000..4867fac22 --- /dev/null +++ b/sendgrid/helpers/mail/bypass_unsubscribe_management.py @@ -0,0 +1,49 @@ +class BypassUnsubscribeManagement(object): + """Setting for Bypass Unsubscribe Management + + + Allows you to bypass the global unsubscribe list to ensure that the email is delivered to recipients. + Bounce and spam report lists will still be checked; addresses on these other lists will not receive + the message. This filter applies only to global unsubscribes and will not bypass group unsubscribes. + This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassUnsubscribeManagement. + + :param enable: Whether emails should bypass unsubscribe management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassUnsubscribeManagement. + + :returns: This BypassUnsubscribeManagement, ready for use in a request body. + :rtype: dict + """ + bypass_unsubscribe_management = {} + if self.enable is not None: + bypass_unsubscribe_management["enable"] = self.enable + return bypass_unsubscribe_management diff --git a/sendgrid/helpers/mail/content.py b/sendgrid/helpers/mail/content.py index 73bf9f64d..618eee917 100644 --- a/sendgrid/helpers/mail/content.py +++ b/sendgrid/helpers/mail/content.py @@ -29,7 +29,7 @@ def __init__(self, mime_type, content): @property def mime_type(self): """The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :rtype: string """ @@ -38,11 +38,11 @@ def mime_type(self): @mime_type.setter def mime_type(self, value): """The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :param value: The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :type value: string """ self._mime_type = value diff --git a/sendgrid/helpers/mail/email.py b/sendgrid/helpers/mail/email.py index ba3a98848..aeab26afa 100644 --- a/sendgrid/helpers/mail/email.py +++ b/sendgrid/helpers/mail/email.py @@ -3,20 +3,6 @@ except ImportError: import email.utils as rfc822 -import sys -if sys.version_info[:3] >= (3, 5, 0): - import html - html_entity_decode = html.unescape -else: - try: - # Python 2.6-2.7 - from HTMLParser import HTMLParser - except ImportError: - # Python < 3.5 - from html.parser import HTMLParser - __html_parser__ = HTMLParser() - html_entity_decode = __html_parser__.unescape - try: basestring = basestring except NameError: @@ -91,11 +77,6 @@ def name(self, value): if not (value is None or isinstance(value, basestring)): raise TypeError('name must be of type string.') - # Escape common CSV delimiters as workaround for - # https://github.com/sendgrid/sendgrid-python/issues/578 - if value is not None and (',' in value or ';' in value): - value = html_entity_decode(value) - value = '"' + value + '"' self._name = value @property diff --git a/sendgrid/helpers/mail/file_content.py b/sendgrid/helpers/mail/file_content.py index c5c0d6995..c1eb81fc6 100644 --- a/sendgrid/helpers/mail/file_content.py +++ b/sendgrid/helpers/mail/file_content.py @@ -31,7 +31,7 @@ def file_content(self, value): def get(self): """ - Get a JSON-ready representation of this FileContente. + Get a JSON-ready representation of this FileContent. :returns: This FileContent, ready for use in a request body. :rtype: string diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index db2399310..e475fe764 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -27,6 +27,7 @@ def __init__( subject=None, plain_text_content=None, html_content=None, + amp_html_content=None, global_substitutions=None, is_multiple=False): """ @@ -43,6 +44,8 @@ def __init__( :type plain_text_content: string, optional :param html_content: The html body of the email :type html_content: string, optional + :param amp_html_content: The amp-html body of the email + :type amp_html_content: string, optional """ self._attachments = None self._categories = None @@ -57,6 +60,7 @@ def __init__( self._ip_pool_name = None self._mail_settings = None self._reply_to = None + self._reply_to_list = None self._send_at = None self._subject = None self._template_id = None @@ -71,6 +75,8 @@ def __init__( self.subject = subject if plain_text_content is not None: self.add_content(plain_text_content, MimeType.text) + if amp_html_content is not None: + self.add_content(amp_html_content, MimeType.amp) if html_content is not None: self.add_content(html_content, MimeType.html) @@ -211,8 +217,8 @@ def to(self): def to(self, to_emails, global_substitutions=None, is_multiple=False, p=0): """Adds To objects to the Personalization object - :param to_emails: An To or list of To objects - :type to_emails: To, list(To), str, tuple + :param to_emails: The email addresses of all recipients + :type to_emails: To, str, tuple, list(str), list(tuple), list(To) :param global_substitutions: A dict of substitutions for all recipients :type global_substitutions: dict :param is_multiple: Create a new personalization for each recipient @@ -561,7 +567,7 @@ def add_custom_arg(self, custom_arg): :param value: A CustomArg object or a dict of custom arg key/values :type value: CustomArg, dict """ - if custom_arg.personalization is not None: + if not isinstance(custom_arg, dict) and custom_arg.personalization is not None: try: personalization = \ self._personalizations[custom_arg.personalization] @@ -690,6 +696,32 @@ def reply_to(self, value): value = ReplyTo(value[0], value[1]) self._reply_to = value + @property + def reply_to_list(self): + """A list of ReplyTo email addresses + + :rtype: list(ReplyTo), tuple + """ + return self._reply_to_list + + @reply_to_list.setter + def reply_to_list(self, value): + """A list of ReplyTo email addresses + + :param value: A list of ReplyTo email addresses + :type value: list(ReplyTo), tuple + """ + if isinstance(value, list): + for reply in value: + if isinstance(reply, ReplyTo): + if not isinstance(reply.email, str): + raise ValueError('You must provide an email for each entry in a reply_to_list') + else: + raise ValueError( + 'Please use a list of ReplyTos for a reply_to_list.' + ) + self._reply_to_list = value + @property def contents(self): """The contents of the email @@ -725,9 +757,23 @@ def add_content(self, content, mime_type=None): """ if isinstance(content, str): content = Content(mime_type, content) - # Content of mime type text/plain must always come first - if content.mime_type == "text/plain": + # Content of mime type text/plain must always come first, followed by text/x-amp-html and then text/html + if content.mime_type == MimeType.text: self._contents = self._ensure_insert(content, self._contents) + elif content.mime_type == MimeType.amp: + if self._contents: + for _content in self._contents: + # this is written in the context that plain text content will always come earlier than the html content + if _content.mime_type == MimeType.text: + index = 1 + break + elif _content.mime_type == MimeType.html: + index = 0 + break + else: + index = 0 + self._contents = self._ensure_append( + content, self._contents, index=index) else: if self._contents: index = len(self._contents) @@ -962,6 +1008,7 @@ def get(self): 'mail_settings': self._get_or_none(self.mail_settings), 'tracking_settings': self._get_or_none(self.tracking_settings), 'reply_to': self._get_or_none(self.reply_to), + 'reply_to_list': [r.get() for r in self.reply_to_list or []], } return {key: value for key, value in mail.items() diff --git a/sendgrid/helpers/mail/mail_settings.py b/sendgrid/helpers/mail/mail_settings.py index 45b7db77f..78499ac30 100644 --- a/sendgrid/helpers/mail/mail_settings.py +++ b/sendgrid/helpers/mail/mail_settings.py @@ -3,7 +3,10 @@ class MailSettings(object): def __init__(self, bcc_settings=None, + bypass_bounce_management=None, bypass_list_management=None, + bypass_spam_management=None, + bypass_unsubscribe_management=None, footer_settings=None, sandbox_mode=None, spam_check=None): @@ -11,9 +14,18 @@ def __init__(self, :param bcc_settings: The BCC Settings of this MailSettings :type bcc_settings: BCCSettings, optional + :param bypass_bounce_management: Whether this MailSettings bypasses bounce management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassBounceManagement, optional :param bypass_list_management: Whether this MailSettings bypasses list management :type bypass_list_management: BypassListManagement, optional + :param bypass_spam_management: Whether this MailSettings bypasses spam management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassSpamManagement, optional + :param bypass_unsubscribe_management: Whether this MailSettings bypasses unsubscribe management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassUnsubscribeManagement, optional :param footer_settings: The default footer specified by this MailSettings :type footer_settings: FooterSettings, optional @@ -24,7 +36,10 @@ def __init__(self, :type spam_check: SpamCheck, optional """ self._bcc_settings = None + self._bypass_bounce_management = None self._bypass_list_management = None + self._bypass_spam_management = None + self._bypass_unsubscribe_management = None self._footer_settings = None self._sandbox_mode = None self._spam_check = None @@ -32,9 +47,18 @@ def __init__(self, if bcc_settings is not None: self.bcc_settings = bcc_settings + if bypass_bounce_management is not None: + self.bypass_bounce_management = bypass_bounce_management + if bypass_list_management is not None: self.bypass_list_management = bypass_list_management + if bypass_spam_management is not None: + self.bypass_spam_management = bypass_spam_management + + if bypass_unsubscribe_management is not None: + self.bypass_unsubscribe_management = bypass_unsubscribe_management + if footer_settings is not None: self.footer_settings = footer_settings @@ -61,6 +85,23 @@ def bcc_settings(self, value): """ self._bcc_settings = value + @property + def bypass_bounce_management(self): + """Whether this MailSettings bypasses bounce management. + + :rtype: BypassBounceManagement + """ + return self._bypass_bounce_management + + @bypass_bounce_management.setter + def bypass_bounce_management(self, value): + """Whether this MailSettings bypasses bounce management. + + :param value: Whether this MailSettings bypasses bounce management. + :type value: BypassBounceManagement + """ + self._bypass_bounce_management = value + @property def bypass_list_management(self): """Whether this MailSettings bypasses list management. @@ -78,6 +119,40 @@ def bypass_list_management(self, value): """ self._bypass_list_management = value + @property + def bypass_spam_management(self): + """Whether this MailSettings bypasses spam management. + + :rtype: BypassSpamManagement + """ + return self._bypass_spam_management + + @bypass_spam_management.setter + def bypass_spam_management(self, value): + """Whether this MailSettings bypasses spam management. + + :param value: Whether this MailSettings bypasses spam management. + :type value: BypassSpamManagement + """ + self._bypass_spam_management = value + + @property + def bypass_unsubscribe_management(self): + """Whether this MailSettings bypasses unsubscribe management. + + :rtype: BypassUnsubscribeManagement + """ + return self._bypass_unsubscribe_management + + @bypass_unsubscribe_management.setter + def bypass_unsubscribe_management(self, value): + """Whether this MailSettings bypasses unsubscribe management. + + :param value: Whether this MailSettings bypasses unsubscribe management. + :type value: BypassUnsubscribeManagement + """ + self._bypass_unsubscribe_management = value + @property def footer_settings(self): """The default footer specified by this MailSettings. @@ -141,10 +216,22 @@ def get(self): if self.bcc_settings is not None: mail_settings["bcc"] = self.bcc_settings.get() + if self.bypass_bounce_management is not None: + mail_settings[ + "bypass_bounce_management"] = self.bypass_bounce_management.get() + if self.bypass_list_management is not None: mail_settings[ "bypass_list_management"] = self.bypass_list_management.get() + if self.bypass_spam_management is not None: + mail_settings[ + "bypass_spam_management"] = self.bypass_spam_management.get() + + if self.bypass_unsubscribe_management is not None: + mail_settings[ + "bypass_unsubscribe_management"] = self.bypass_unsubscribe_management.get() + if self.footer_settings is not None: mail_settings["footer"] = self.footer_settings.get() diff --git a/sendgrid/helpers/mail/mime_type.py b/sendgrid/helpers/mail/mime_type.py index 0d0c9b3b3..a2f88c5af 100644 --- a/sendgrid/helpers/mail/mime_type.py +++ b/sendgrid/helpers/mail/mime_type.py @@ -3,3 +3,4 @@ class MimeType(object): """ text = "text/plain" html = "text/html" + amp = "text/x-amp-html" diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 9239f9458..a4e1c1de4 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -6,6 +6,7 @@ class Personalization(object): def __init__(self): """Create an empty Personalization and initialize member variables.""" self._tos = [] + self._from_email = None self._ccs = [] self._bccs = [] self._subject = None @@ -26,7 +27,24 @@ def add_email(self, email): if email_type.__name__ == 'Bcc': self.add_bcc(email) return - raise ValueError('Please use a To, Cc or Bcc object.') + if email_type.__name__ == 'From': + self.from_email = email + return + raise ValueError('Please use a To, From, Cc or Bcc object.') + + def _get_unique_recipients(self, recipients): + unique_recipients = [] + + for recipient in recipients: + recipient_email = recipient['email'].lower() if isinstance(recipient, dict) else recipient.email.lower() + if all( + unique_recipient['email'].lower() != recipient_email for unique_recipient in unique_recipients + ): + new_unique_recipient = recipient if isinstance(recipient, dict) else recipient.get() + unique_recipients.append(new_unique_recipient) + + return unique_recipients + @property def tos(self): @@ -34,7 +52,7 @@ def tos(self): :rtype: list(dict) """ - return self._tos + return self._get_unique_recipients(self._tos) @tos.setter def tos(self, value): @@ -63,13 +81,24 @@ def add_to(self, email): self._tos.append(email.get()) + @property + def from_email(self): + return self._from_email + + @from_email.setter + def from_email(self, value): + self._from_email = value + + def set_from(self, email): + self._from_email = email.get() + @property def ccs(self): """A list of recipients who will receive copies of this email. :rtype: list(dict) """ - return self._ccs + return self._get_unique_recipients(self._ccs) @ccs.setter def ccs(self, value): @@ -89,7 +118,7 @@ def bccs(self): :rtype: list(dict) """ - return self._bccs + return self._get_unique_recipients(self._bccs) @bccs.setter def bccs(self, value): @@ -222,6 +251,10 @@ def get(self): if value: personalization[key[:-1]] = value + from_value = getattr(self, 'from_email') + if from_value: + personalization['from'] = from_value + for key in ['subject', 'send_at', 'dynamic_template_data']: value = getattr(self, key) if value: diff --git a/sendgrid/helpers/stats/README.md b/sendgrid/helpers/stats/README.md index 4ef738410..f1591ecce 100644 --- a/sendgrid/helpers/stats/README.md +++ b/sendgrid/helpers/stats/README.md @@ -2,9 +2,9 @@ # Quick Start -Run the [example](https://github.com/sendgrid/sendgrid-python/tree/master/examples/helpers/stats) (make sure you have set your environment variable to include your SENDGRID_API_KEY). +Run the [example](../../../examples/helpers/stats) (make sure you have set your environment variable to include your SENDGRID_API_KEY). ## Usage -- See the [examples](https://github.com/sendgrid/sendgrid-python/tree/master/examples/helpers/stats) for complete working examples. +- See the [examples](../../../examples/helpers/stats) for complete working examples. - [Documentation](https://sendgrid.com/docs/API_Reference/Web_API_v3/Stats/index.html) diff --git a/sendgrid/version.py b/sendgrid/version.py index 9bf999671..c1d623f90 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.4.5' +__version__ = '6.12.5' diff --git a/setup.py b/setup.py index b3eeec82d..904cd654a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import io import os -from distutils.file_util import copy_file from setuptools import setup, find_packages @@ -11,7 +10,14 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'starkbank-ecdsa>=1.0.0' + 'cryptography>=45.0.6', + "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", + "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.7'", + "werkzeug>=0.15.0,<2.3.0 ; python_version >= '3.0' and python_version < '3.8'", # version 2.3.0 dropped support for Python 3.7 + "werkzeug>=0.16.0,<3.1.0 ; python_version >= '3.0' and python_version < '3.9'", # version 3.1.0 dropped support for Python 3.8 + "werkzeug>=1.0.0 ; python_version >= '3.9'", + "werkzeug>=2.2.0 ; python_version >= '3.11'", + "werkzeug>=2.3.5 ; python_version >= '3.12'" ] return deps @@ -35,9 +41,14 @@ def getRequires(): classifiers=[ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] ) diff --git a/static/img/github-fork.png b/static/img/github-fork.png new file mode 100644 index 000000000..6503be362 Binary files /dev/null and b/static/img/github-fork.png differ diff --git a/static/img/github-sign-up.png b/static/img/github-sign-up.png new file mode 100644 index 000000000..491392b8a Binary files /dev/null and b/static/img/github-sign-up.png differ diff --git a/test/integ/__init__.py b/test/integ/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_sendgrid.py b/test/integ/test_sendgrid.py similarity index 99% rename from test/test_sendgrid.py rename to test/integ/test_sendgrid.py index eb9ebabb3..0c63851eb 100644 --- a/test/test_sendgrid.py +++ b/test/integ/test_sendgrid.py @@ -1649,14 +1649,14 @@ def test_suppression_invalid_emails__email__delete(self): def test_suppression_spam_report__email__get(self): email = "test_url_param" headers = {'X-Mock': 200} - response = self.sg.client.suppression.spam_report._( + response = self.sg.client.suppression.spam_reports._( email).get(request_headers=headers) self.assertEqual(response.status_code, 200) def test_suppression_spam_report__email__delete(self): email = "test_url_param" headers = {'X-Mock': 204} - response = self.sg.client.suppression.spam_report._( + response = self.sg.client.suppression.spam_reports._( email).delete(request_headers=headers) self.assertEqual(response.status_code, 204) @@ -2297,7 +2297,7 @@ def test_whitelabel_links__link_id__subuser_post(self): self.assertEqual(response.status_code, 200) def test_license_year(self): - LICENSE_FILE = 'LICENSE.md' + LICENSE_FILE = 'LICENSE' copyright_line = '' with open(LICENSE_FILE, 'r') as f: for line in f: diff --git a/test/requirements.txt b/test/requirements.txt index 6cb2e96d2..40552deba 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,8 @@ pyyaml -flask +Flask==1.1.4 six coverage -codecov mock +itsdangerous==1.1.0 +markupsafe==1.1.1 +setuptools diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_app.py b/test/unit/test_app.py similarity index 100% rename from test/test_app.py rename to test/unit/test_app.py diff --git a/test/test_config.py b/test/unit/test_config.py similarity index 100% rename from test/test_config.py rename to test/unit/test_config.py diff --git a/test/test_email.py b/test/unit/test_email.py similarity index 84% rename from test/test_email.py rename to test/unit/test_email.py index eb50374aa..9db060705 100644 --- a/test/test_email.py +++ b/test/unit/test_email.py @@ -66,17 +66,3 @@ def test_empty_obj_add_email(self): email.email = address self.assertEqual(email.email, address) - - def test_add_name_with_comma(self): - email = Email() - name = "Name, Some" - email.name = name - - self.assertEqual(email.name, '"' + name + '"') - - def test_add_unicode_name_with_comma(self): - email = Email() - name = u"Name, Some" - email.name = name - - self.assertEqual(email.name, u'"' + name + u'"') diff --git a/test/test_eventwebhook.py b/test/unit/test_eventwebhook.py similarity index 56% rename from test/test_eventwebhook.py rename to test/unit/test_eventwebhook.py index 28f9ad282..eee1eabf9 100644 --- a/test/test_eventwebhook.py +++ b/test/unit/test_eventwebhook.py @@ -7,14 +7,22 @@ class UnitTests(unittest.TestCase): @classmethod def setUpClass(cls): - cls.PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==' - cls.SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=' - cls.TIMESTAMP = '1588788367' - cls.PAYLOAD = json.dumps({ - 'event': 'test_event', - 'category': 'example_payload', - 'message_id': 'message_id', - }, sort_keys=True, separators=(',', ':')) + cls.PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==' + cls.SIGNATURE = 'MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=' + cls.TIMESTAMP = '1600112502' + cls.PAYLOAD = json.dumps( + [ + { + 'email': 'hello@world.com', + 'event': 'dropped', + 'reason': 'Bounced Address', + 'sg_event_id': 'ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA', + 'sg_message_id': 'LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0', + 'smtp-id': '', + 'timestamp': 1600112492, + } + ], sort_keys=True, separators=(',', ':') + ) + '\r\n' # Be sure to include the trailing carriage return and newline! def test_verify_valid_signature(self): ew = EventWebhook() diff --git a/test/test_inbound_send.py b/test/unit/test_inbound_send.py similarity index 100% rename from test/test_inbound_send.py rename to test/unit/test_inbound_send.py diff --git a/test/test_mail_helpers.py b/test/unit/test_mail_helpers.py similarity index 64% rename from test/test_mail_helpers.py rename to test/unit/test_mail_helpers.py index 202d3948b..c00307d46 100644 --- a/test/test_mail_helpers.py +++ b/test/unit/test_mail_helpers.py @@ -17,6 +17,40 @@ Subject, Substitution, To, Cc, Bcc, TrackingSettings ) +# The below amp html email content is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/) +amp_html_content = '''

Hello!

''' + +response_content_with_all_three_mime_contents = json.dumps({ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/x-amp-html", + "value": amp_html_content + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" +}) class UnitTests(unittest.TestCase): @@ -201,6 +235,58 @@ def test_send_a_single_email_to_multiple_recipients(self): }''') ) + def test_send_a_single_email_with_multiple_reply_to_addresses(self): + from sendgrid.helpers.mail import (Mail, From, ReplyTo, To, Subject, + PlainTextContent, HtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to0@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), + html_content=HtmlContent('and easy to do anywhere, even with Python')) + + message.reply_to_list = [ReplyTo(email = 'test+reply_to_1@example.com'), ReplyTo(email = 'test+reply_to_2@example.com')] + + self.assertEqual( + message.get(), + json.loads(r'''{ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to0@example.com", + "name": "Example To Name" + } + ] + } + ], + "reply_to_list": [ + { + "email": "test+reply_to_1@example.com" + }, + { + "email": "test+reply_to_2@example.com" + } + ], + "subject": "Sending with SendGrid is Fun" + }''') + ) + def test_multiple_emails_to_multiple_recipients(self): from sendgrid.helpers.mail import (Mail, From, To, Subject, PlainTextContent, HtmlContent, @@ -284,6 +370,239 @@ def test_multiple_emails_to_multiple_recipients(self): }''') ) + def test_single_email_with_all_three_email_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + amp_html_content=AmpHtmlContent(amp_html_content), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python') + ) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_amp_and_html_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + amp_html_content=AmpHtmlContent(amp_html_content), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python') + ) + + response_content = json.dumps({ + "content": [ + { + "type": "text/x-amp-html", + "value": amp_html_content + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" + }) + self.assertEqual( + message.get(), + json.loads(response_content) + ) + + def test_single_email_with_amp_and_plain_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + amp_html_content=AmpHtmlContent(amp_html_content) + ) + + response_content = json.dumps({ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/x-amp-html", + "value": amp_html_content + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" + }) + self.assertEqual( + message.get(), + json.loads(response_content) + ) + + ## Check ordering of MIME types in different variants - start + def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_amp_html_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_html_amp_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_html_plain_amp_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_html_amp_plain_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_html_plain_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = AmpHtmlContent(amp_html_content) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_plain_html_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = AmpHtmlContent(amp_html_content) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + ## end + def test_value_error_is_raised_on_to_emails_set_to_list_of_lists(self): from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) self.maxDiff = None @@ -301,6 +620,44 @@ def test_value_error_is_raised_on_to_emails_set_to_list_of_lists(self): 'and easy to do anywhere, even with Python'), html_content=HtmlContent( 'and easy to do anywhere, even with Python')) + + def test_value_error_is_raised_on_to_emails_set_to_reply_to_list_of_strs(self): + from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) + self.maxDiff = None + to_emails = [ + ('test+to0@example.com', 'Example To Name 0'), + ('test+to1@example.com', 'Example To Name 1') + ] + + mail = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=to_emails, + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python')) + with self.assertRaises(ValueError): + mail.reply_to_list = ['test+reply_to0@example.com', 'test+reply_to1@example.com'] + + def test_value_error_is_raised_on_to_emails_set_to_reply_to_list_of_tuples(self): + from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) + self.maxDiff = None + to_emails = [ + ('test+to0@example.com', 'Example To Name 0'), + ('test+to1@example.com', 'Example To Name 1') + ] + + mail = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=to_emails, + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python')) + with self.assertRaises(ValueError): + mail.reply_to_list = [('test+reply_to@example.com', 'Test Name')] def test_error_is_not_raised_on_to_emails_set_to_list_of_tuples(self): from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) @@ -379,6 +736,233 @@ def test_error_is_not_raised_on_to_emails_includes_bcc_cc(self): html_content=HtmlContent( 'and easy to do anywhere, even with Python')) + def test_personalization_add_email_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + p.add_email(to_email) + p.add_email(to_email) + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_add_email_filters_out_duplicate_to_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + to_email_with_caps = To('test+TO0@example.com', 'Example To Name 0') + p.add_email(to_email) + p.add_email(to_email_with_caps) + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_set_from_email(self): + self.maxDiff = None + + p = Personalization() + from_email = From('test+from@example.com', 'Example From') + p.set_from(from_email) + + self.assertEqual(from_email.get(), p.from_email) + + def test_personalization_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + p.add_email(cc_email) + p.add_email(cc_email) + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_filters_out_duplicate_cc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + cc_email_with_caps = Cc('test+CC0@example.com', 'Example Cc Name 0') + p.add_email(cc_email) + p.add_email(cc_email_with_caps) + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + p.add_email(bcc_email) + p.add_email(bcc_email) + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_filters_out_duplicate_bcc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + bcc_email_with_caps = Bcc('test+BCC0@example.com', 'Example Bcc Name 0') + p.add_email(bcc_email) + p.add_email(bcc_email_with_caps) + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_tos_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + to_emails = [{ 'email': 'test+to0@example.com', 'name': 'Example To Name 0' }] * 2 + p.tos = to_emails + + self.assertEqual([to_emails[0]], p.tos) + + def test_personalization_tos_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = { 'email': 'test+to0@example.com', 'name': 'Example To Name 0' } + to_email_with_caps = { 'email': 'test+TO0@example.com', 'name': 'Example To Name 0' } + to_emails = [to_email, to_email_with_caps] + p.tos = to_emails + + self.assertEqual([to_email], p.tos) + + def test_personalization_tos_setter_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_emails = [To('test+to0@example.com', 'Example To Name 0')] * 2 + p.tos = to_emails + + self.assertEqual([to_emails[0].get()], p.tos) + + + def test_personalization_tos_setter_filters_out_duplicate_to_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + to_email_with_caps = To('test+TO0@example.com', 'Example To Name 0') + to_emails = [to_email, to_email_with_caps] + p.tos = to_emails + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_ccs_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + cc_emails = [{ 'email': 'test+cc0@example.com', 'name': 'Example Cc Name 0' }] * 2 + p.ccs = cc_emails + + self.assertEqual([cc_emails[0]], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = { 'email': 'test+cc0@example.com', 'name': 'Example Cc Name 0' } + cc_email_with_caps = { 'email': 'test+CC0@example.com', 'name': 'Example Cc Name 0' } + cc_emails = [cc_email, cc_email_with_caps] + p.ccs = cc_emails + + self.assertEqual([cc_email], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_emails = [Cc('test+cc0@example.com', 'Example Cc Name 0')] * 2 + p.ccs = cc_emails + + self.assertEqual([cc_emails[0].get()], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_cc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + cc_email_with_caps = Cc('test+CC0@example.com', 'Example Cc Name 0') + p.ccs = [cc_email, cc_email_with_caps] + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_bccs_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_emails = [{ 'email': 'test+bcc0@example.com', 'name': 'Example Bcc Name 0' }] * 2 + p.bccs = bcc_emails + + self.assertEqual([bcc_emails[0]], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = { 'email': 'test+bcc0@example.com', 'name': 'Example Bcc Name 0' } + bcc_email_with_caps = { 'email': 'test+BCC0@example.com', 'name': 'Example Bcc Name 0' } + bcc_emails = [bcc_email, bcc_email_with_caps] + p.bccs = bcc_emails + + self.assertEqual([bcc_email], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_emails = [Bcc('test+bcc0@example.com', 'Example Bcc Name 0')] * 2 + p.bccs = bcc_emails + + self.assertEqual([bcc_emails[0].get()], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_bcc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + bcc_email_with_caps = Bcc('test+BCC0@example.com', 'Example Bcc Name 0') + p.bccs = [bcc_email, bcc_email_with_caps] + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_add_to_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + p.add_to(to_email) + p.add_to(to_email) + + expected = [to_email.get()] + + self.assertEqual(expected, p.tos) + + def test_personalization_add_bcc_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+to0@example.com', 'Example To Name 0') + p.add_bcc(bcc_email) + p.add_bcc(bcc_email) + + expected = [bcc_email.get()] + + self.assertEqual(expected, p.bccs) + + def test_personalization_add_cc_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+to0@example.com', 'Example To Name 0') + p.add_cc(cc_email) + p.add_cc(cc_email) + + expected = [cc_email.get()] + + self.assertEqual(expected, p.ccs) + def test_dynamic_template_data(self): self.maxDiff = None @@ -450,7 +1034,8 @@ def test_kitchen_sink(self): FileContent, FileType, Disposition, ContentId, TemplateId, Section, ReplyTo, Category, BatchId, Asm, GroupId, GroupsToDisplay, IpPoolName, MailSettings, BccSettings, BccSettingsEmail, - BypassListManagement, FooterSettings, FooterText, + BypassBounceManagement, BypassListManagement, BypassSpamManagement, + BypassUnsubscribeManagement, FooterSettings, FooterText, FooterHtml, SandBoxMode, SpamCheck, SpamThreshold, SpamUrl, TrackingSettings, ClickTracking, SubscriptionTracking, SubscriptionText, SubscriptionHtml, SubscriptionSubstitutionTag, @@ -631,7 +1216,10 @@ def test_kitchen_sink(self): mail_settings = MailSettings() mail_settings.bcc_settings = BccSettings( False, BccSettingsEmail("bcc@twilio.com")) + mail_settings.bypass_bounce_management = BypassBounceManagement(False) mail_settings.bypass_list_management = BypassListManagement(False) + mail_settings.bypass_spam_management = BypassSpamManagement(False) + mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(False) mail_settings.footer_settings = FooterSettings( True, FooterText("w00t"), FooterHtml("w00t!")) mail_settings.sandbox_mode = SandBoxMode(True) @@ -738,9 +1326,18 @@ def test_kitchen_sink(self): "email": "bcc@twilio.com", "enable": false }, + "bypass_bounce_management": { + "enable": false + }, "bypass_list_management": { "enable": false }, + "bypass_spam_management": { + "enable": false + }, + "bypass_unsubscribe_management": { + "enable": false + }, "footer": { "enable": true, "html": "w00t!", @@ -1128,3 +1725,42 @@ def test_disable_tracking(self): tracking_settings.get(), {'click_tracking': {'enable': False, 'enable_text': False}} ) + + def test_bypass_list_management(self): + from sendgrid.helpers.mail import (MailSettings, BypassListManagement) + mail_settings = MailSettings() + mail_settings.bypass_list_management = BypassListManagement(True) + + self.assertEqual( + mail_settings.get(), + { + "bypass_list_management": { + "enable": True + }, + }, + ) + + def test_v3_bypass_filters(self): + from sendgrid.helpers.mail import ( + MailSettings, BypassBounceManagement, + BypassSpamManagement, BypassUnsubscribeManagement + ) + mail_settings = MailSettings() + mail_settings.bypass_bounce_management = BypassBounceManagement(True) + mail_settings.bypass_spam_management = BypassSpamManagement(True) + mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(True) + + self.assertEqual( + mail_settings.get(), + { + "bypass_bounce_management": { + "enable": True + }, + "bypass_spam_management": { + "enable": True + }, + "bypass_unsubscribe_management": { + "enable": True + }, + }, + ) diff --git a/test/test_parse.py b/test/unit/test_parse.py similarity index 100% rename from test/test_parse.py rename to test/unit/test_parse.py diff --git a/test/test_project.py b/test/unit/test_project.py similarity index 75% rename from test/test_project.py rename to test/unit/test_project.py index 79f27972d..40282bdb7 100644 --- a/test/test_project.py +++ b/test/unit/test_project.py @@ -11,14 +11,6 @@ def test_env(self): def test_gitignore(self): self.assertTrue(os.path.isfile('./.gitignore')) - # ./.travis.yml - def test_travis(self): - self.assertTrue(os.path.isfile('./.travis.yml')) - - # ./.codeclimate.yml - def test_codeclimate(self): - self.assertTrue(os.path.isfile('./.codeclimate.yml')) - # ./CHANGELOG.md def test_changelog(self): self.assertTrue(os.path.isfile('./CHANGELOG.md')) @@ -31,13 +23,9 @@ def test_code_of_conduct(self): def test_contributing(self): self.assertTrue(os.path.isfile('./CONTRIBUTING.md')) - # ./ISSUE_TEMPLATE.md - def test_issue_template(self): - self.assertTrue(os.path.isfile('./ISSUE_TEMPLATE.md')) - - # ./LICENSE.md + # ./LICENSE def test_license(self): - self.assertTrue(os.path.isfile('./LICENSE.md')) + self.assertTrue(os.path.isfile('./LICENSE')) # ./PULL_REQUEST_TEMPLATE.md def test_pr_template(self): diff --git a/test/unit/test_sendgrid.py b/test/unit/test_sendgrid.py new file mode 100644 index 000000000..328d978ab --- /dev/null +++ b/test/unit/test_sendgrid.py @@ -0,0 +1,27 @@ +import unittest +import sendgrid + +class UnitTests(unittest.TestCase): + def test_host_with_no_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + self.assertEqual("https://api.sendgrid.com",sg.client.host) + + def test_host_with_eu_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + sg.set_sendgrid_data_residency("eu") + self.assertEqual("https://api.eu.sendgrid.com",sg.client.host) + + def test_host_with_global_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + sg.set_sendgrid_data_residency("global") + self.assertEqual("https://api.sendgrid.com",sg.client.host) + + def test_with_region_is_none(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + with self.assertRaises(ValueError): + sg.set_sendgrid_data_residency(None) + + def test_with_region_is_invalid(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + with self.assertRaises(ValueError): + sg.set_sendgrid_data_residency("abc") \ No newline at end of file diff --git a/test/test_spam_check.py b/test/unit/test_spam_check.py similarity index 100% rename from test/test_spam_check.py rename to test/unit/test_spam_check.py diff --git a/test/test_stats.py b/test/unit/test_stats.py similarity index 100% rename from test/test_stats.py rename to test/unit/test_stats.py diff --git a/test/test_twilio_email.py b/test/unit/test_twilio_email.py similarity index 100% rename from test/test_twilio_email.py rename to test/unit/test_twilio_email.py diff --git a/test/test_unassigned.py b/test/unit/test_unassigned.py similarity index 96% rename from test/test_unassigned.py rename to test/unit/test_unassigned.py index 6054447d8..08ab943bb 100644 --- a/test/test_unassigned.py +++ b/test/unit/test_unassigned.py @@ -1,9 +1,7 @@ import json -import pytest from sendgrid.helpers.endpoints.ip.unassigned import unassigned - ret_json = '''[ { "ip": "167.89.21.3", "pools": [ @@ -69,8 +67,7 @@ def make_data(): def test_unassigned_ip_json(): - - data = make_data() + data = make_data() as_json = True calculated = unassigned(get_all_ip(), as_json=as_json) @@ -81,8 +78,7 @@ def test_unassigned_ip_json(): def test_unassigned_ip_obj(): - - data = make_data() + data = make_data() as_json = False calculated = unassigned(get_all_ip(), as_json=as_json) diff --git a/tox.ini b/tox.ini index 926a3c9dd..8f4f2db9a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, py37 +envlist = py27, py34, py35, py36, py37, py38, py39, py310, py311, py312, py313 [testenv] commands = coverage erase @@ -39,3 +39,33 @@ basepython = python3.6 commands = {[testenv]commands} deps = {[testenv]deps} basepython = python3.7 + +[testenv:py38] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.8 + +[testenv:py39] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.9 + +[testenv:py310] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.10 + +[testenv:py311] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.11 + +[testenv:py312] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.12 + +[testenv:py313] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.13 diff --git a/use_cases/README.md b/use_cases/README.md index a91f1f5a4..f9fe2470e 100644 --- a/use_cases/README.md +++ b/use_cases/README.md @@ -8,6 +8,7 @@ This directory provides examples for specific use cases of this library. Please * [Send a Single Email to a Single Recipient](send_a_single_email_to_a_single_recipient.md) * [Send a Single Email to Multiple Recipients](send_a_single_email_to_multiple_recipients.md) * [Send Multiple Emails to Multiple Recipients](send_multiple_emails_to_multiple_recipients.md) +* [Send Multiple Emails with Personalizations](send_multiple_emails_personalizations.md) * [Kitchen Sink - an example with all settings used](kitchen_sink.md) * [Transactional Templates](transactional_templates.md) * [Attachments](attachment.md) diff --git a/use_cases/django.md b/use_cases/django.md index c71175faa..809b491a6 100644 --- a/use_cases/django.md +++ b/use_cases/django.md @@ -196,7 +196,7 @@ Commit the code to the repository and deploy it to Heroku using Git. ``` $ git add . $ git commit -am "Create simple Hello Email Django app using Twilio SendGrid" -$ git push heroku master +$ git push heroku main ``` After that, let's verify if our app is working or not by accessing the root domain of your Heroku app. You should see the page says "Email Sent!" and on the Activity Feed page in the Twilio SendGrid dashboard, you should see a new feed with the email you set in the code. diff --git a/use_cases/domain_authentication.md b/use_cases/domain_authentication.md index d34470b39..4740f92f9 100644 --- a/use_cases/domain_authentication.md +++ b/use_cases/domain_authentication.md @@ -1,5 +1,5 @@ # How to Setup a Domain Authentication -You can find documentation for how to setup a domain authentication via the UI [here](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication) and via API [here](https://github.com/sendgrid/sendgrid-python/blob/master/USAGE.md#sender-authentication). +You can find documentation for how to setup a domain authentication via the UI [here](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication) and via API [here](../USAGE.md#sender-authentication). Find more information about all of Twilio SendGrid's domain authentication related documentation [here](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication). diff --git a/use_cases/email_stats.md b/use_cases/email_stats.md index 7799710d2..10e265721 100644 --- a/use_cases/email_stats.md +++ b/use_cases/email_stats.md @@ -1,5 +1,5 @@ # How to View Email Statistics -You can find documentation for how to view your email statistics via the UI [here](https://app.sendgrid.com/statistics) and via API [here](https://github.com/sendgrid/sendgrid-python/blob/master/USAGE.md#stats). +You can find documentation for how to view your email statistics via the UI [here](https://app.sendgrid.com/statistics) and via API [here](../USAGE.md#stats). -Alternatively, we can post events to a URL of your choice via our [Event Webhook](https://sendgrid.com/docs/API_Reference/Webhooks/event.html) about events that occur as Twilio SendGrid processes your email. \ No newline at end of file +Alternatively, we can post events to a URL of your choice via our [Event Webhook](https://docs.sendgrid.com/for-developers/tracking-events/event) about events that occur as Twilio SendGrid processes your email. diff --git a/use_cases/error_handling.md b/use_cases/error_handling.md index eaeabad18..d0bdf0945 100644 --- a/use_cases/error_handling.md +++ b/use_cases/error_handling.md @@ -1,29 +1,29 @@ # Error Handling -[Custom exceptions](https://github.com/sendgrid/python-http-client/blob/master/python_http_client/exceptions.py) for `python_http_client` are now supported. +[Custom exceptions](https://github.com/sendgrid/python-http-client/blob/HEAD/python_http_client/exceptions.py) for `python_http_client` are now supported. -Please see [here](https://github.com/sendgrid/python-http-client/blob/master/python_http_client/exceptions.py) for a list of supported exceptions. +Please see [here](https://github.com/sendgrid/python-http-client/blob/HEAD/python_http_client/exceptions.py) for a list of supported exceptions. -There are also email specific exceptions located [here](https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/mail/exceptions.py) +There are also email specific exceptions located [here](../sendgrid/helpers/mail/exceptions.py) ```python - import os - from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import (From, To, Subject, PlainTextContent, HtmlContent, Mail) - from python_http_client import exceptions +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import (From, To, Subject, PlainTextContent, HtmlContent, Mail) +from python_http_client import exceptions - sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) - from_email = From("help@twilio.com") - to_email = To("ethomas@twilio.com") - subject = Subject("Sending with Twilio SendGrid is Fun") - plain_text_content = PlainTextContent("and easy to do anywhere, even with Python") - html_content = HtmlContent("and easy to do anywhere, even with Python") - message = Mail(from_email, to_email, subject, plain_text_content, html_content) - try: - response = sendgrid_client.send(message) - print(response.status_code) - print(response.body) - print(response.headers) - except exceptions.BadRequestsError as e: - print(e.body) - exit() +sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) +from_email = From("help@twilio.com") +to_email = To("ethomas@twilio.com") +subject = Subject("Sending with Twilio SendGrid is Fun") +plain_text_content = PlainTextContent("and easy to do anywhere, even with Python") +html_content = HtmlContent("and easy to do anywhere, even with Python") +message = Mail(from_email, to_email, subject, plain_text_content, html_content) +try: + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except exceptions.BadRequestsError as e: + print(e.body) + exit() ``` diff --git a/use_cases/kitchen_sink.md b/use_cases/kitchen_sink.md index 68c9e057e..c0a301117 100644 --- a/use_cases/kitchen_sink.md +++ b/use_cases/kitchen_sink.md @@ -8,7 +8,8 @@ from sendgrid.helpers.mail import ( FileContent, FileType, Disposition, ContentId, TemplateId, Section, ReplyTo, Category, BatchId, Asm, GroupId, GroupsToDisplay, IpPoolName, MailSettings, BccSettings, BccSettingsEmail, - BypassListManagement, FooterSettings, FooterText, + BypassBounceManagement, BypassListManagement, BypassSpamManagement, + BypassUnsubscribeManagement, FooterSettings, FooterText, FooterHtml, SandBoxMode, SpamCheck, SpamThreshold, SpamUrl, TrackingSettings, ClickTracking, SubscriptionTracking, SubscriptionText, SubscriptionHtml, SubscriptionSubstitutionTag, @@ -185,7 +186,10 @@ mail_settings = MailSettings() mail_settings.bcc_settings = BccSettings( False, BccSettingsEmail("bcc@twilio.com")) +mail_settings.bypass_bounce_management = BypassBounceManagement(False) mail_settings.bypass_list_management = BypassListManagement(False) +mail_settings.bypass_spam_management = BypassSpamManagement(False) +mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(False) mail_settings.footer_settings = FooterSettings( True, FooterText("w00t"), diff --git a/use_cases/legacy_templates.md b/use_cases/legacy_templates.md index c1c9204fa..c8188a257 100644 --- a/use_cases/legacy_templates.md +++ b/use_cases/legacy_templates.md @@ -1,6 +1,6 @@ # Legacy Templates -For this example, we assume you have created a [legacy template](https://sendgrid.com/docs/ui//sending-email/create-and-edit-legacy-transactional-templates). Following is the template content we used for testing. +For this example, we assume you have created a [legacy transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html) in the UI or via the API. Following is the template content we used for testing. Template ID (replace with your own): @@ -113,4 +113,4 @@ except urllib.HTTPError as e: print(response.status_code) print(response.body) print(response.headers) -``` \ No newline at end of file +``` diff --git a/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md b/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md new file mode 100644 index 000000000..55d6adf18 --- /dev/null +++ b/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md @@ -0,0 +1,29 @@ +```python +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail + +message = Mail( + from_email='from_email@example.com', + to_emails='to@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python') +message.reply_to_list = [ + ReplyTo( + email='reply-to-1@example.com', + name="Reply To Name 1", + ), + ReplyTo( + email='reply-to-2@example.com', + name="Reply To Name 2", + ) +] +try: + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e) +``` diff --git a/use_cases/send_multiple_emails_personalizations.md b/use_cases/send_multiple_emails_personalizations.md new file mode 100644 index 000000000..e8a7e2eec --- /dev/null +++ b/use_cases/send_multiple_emails_personalizations.md @@ -0,0 +1,32 @@ +```python +import os +import json +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, Personalization, From, To, Cc, Bcc + +# Note that the domain for all From email addresses must match +message = Mail( + from_email=('from@example.com', 'Example From Name'), + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python') + +personalization1 = Personalization() +personalization1.add_email(To('test0@example.com', 'Example Name 0')) +personalization1.add_email(Cc('test1@example.com', 'Example Name 1')) +message.add_personalization(personalization1) + +personalization2 = Personalization() +personalization2.add_email(To('test2@example.com', 'Example Name 2')) +personalization2.add_email(Bcc('test3@example.com', 'Example Name 3')) +personalization2.add_email(From('from2@example.com', 'Example From Name 2')) +message.add_personalization(personalization2) + +try: + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e.message) +``` \ No newline at end of file diff --git a/use_cases/sending_amp_html_content.md b/use_cases/sending_amp_html_content.md new file mode 100644 index 000000000..616b52039 --- /dev/null +++ b/use_cases/sending_amp_html_content.md @@ -0,0 +1,102 @@ +# Sending AMP-HTML Email + +Following is an example on how to send an AMP HTML Email. +Currently, we require AMP HTML and any one of HTML or Plain Text content (preferrably both) for improved deliverability or fallback for AMP HTML Email for supporting older clients and showing alternate content after 30 days. + +For more information on AMP emails pls check the [official AMP email page](https://amp.dev/about/email/) + +```python +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail + +# The below amp html email is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/) +amp_html_content = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Hello!

+ + + + + + + + +
+ + +''' + +message = Mail( + from_email='example@example.com', + to_emails='example@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python', + amp_html_content=amp_html_content) +try: + sg = SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) + response = sg.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e.message) +``` \ No newline at end of file diff --git a/use_cases/sending_html_content.md b/use_cases/sending_html_content.md index ba38b19aa..4a828e737 100644 --- a/use_cases/sending_html_content.md +++ b/use_cases/sending_html_content.md @@ -35,14 +35,13 @@ html_text = """ sendgrid_client = SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) from_email = From("from_email@exmaple.com") -to_email = Email("to_email@example.com") +to_email = To("to_email@example.com") subject = Subject("Test Subject") html_content = HtmlContent(html_text) soup = BeautifulSoup(html_text) plain_text = soup.get_text() plain_text_content = Content("text/plain", plain_text) -mail.add_content(plain_content) message = Mail(from_email, to_email, subject, plain_text_content, html_content) @@ -54,4 +53,4 @@ try: except urllib.HTTPError as e: print(e.read()) exit() -``` \ No newline at end of file +``` diff --git a/use_cases/transactional_templates.md b/use_cases/transactional_templates.md index 2a237b946..460fd65ee 100644 --- a/use_cases/transactional_templates.md +++ b/use_cases/transactional_templates.md @@ -1,6 +1,6 @@ # Transactional Templates -For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html) in the UI or via the API. Following is the template content we used for testing. +For this example, we assume you have created a [dynamic transactional template](https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/) in the UI or via the API. Following is the template content we used for testing. Email Subject: @@ -28,7 +28,6 @@ I hope you are having a great day in {{ city }} :) ```python import os -import json from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail @@ -86,7 +85,6 @@ I hope you are having a great day in {{{ city }}} :) ```python import os -import json from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail