├── .env_sample ├── .github └── workflows │ ├── pr-lint.yml │ └── test-and-deploy.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FIRST_TIMERS.md ├── LICENSE ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── TROUBLESHOOTING.md ├── USAGE.md ├── examples └── example.go ├── go.test.sh ├── smtpapi.go ├── smtpapi_test.go ├── smtpapi_test_strings.json ├── static └── img │ ├── github-fork.png │ └── github-sign-up.png ├── twilio_sendgrid_logo.png └── use_cases └── README.md /.env_sample: -------------------------------------------------------------------------------- 1 | export SENDGRID_API_KEY='' -------------------------------------------------------------------------------- /.github/workflows/pr-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR 2 | on: 3 | pull_request_target: 4 | types: [ opened, edited, synchronize, reopened ] 5 | 6 | jobs: 7 | validate: 8 | name: Validate title 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: amannn/action-semantic-pull-request@v4 12 | with: 13 | types: chore docs fix feat test misc 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/test-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | on: 3 | push: 4 | branches: [ '*' ] 5 | tags: [ '*' ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Run automatically at 8AM PST Monday-Friday 10 | - cron: '0 15 * * 1-5' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 20 18 | strategy: 19 | matrix: 20 | go: [ '1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17' ] 21 | steps: 22 | - name: Checkout smtpapi-go 23 | uses: actions/checkout@v2 24 | 25 | - name: Setup Go environment 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ${{ matrix.go }} 29 | 30 | - name: Set Go env vars 31 | run: | 32 | echo "GOPATH=$HOME" >> $GITHUB_ENV 33 | echo "GOBIN=$HOME/bin" >> $GITHUB_ENV 34 | echo "GO111MODULE=off" >> $GITHUB_ENV 35 | 36 | - name: Run Tests 37 | run: make test 38 | 39 | deploy: 40 | name: Deploy 41 | if: success() && github.ref_type == 'tag' 42 | needs: [ test ] 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout smtpapi-go 46 | uses: actions/checkout@v2 47 | 48 | - name: Create GitHub Release 49 | uses: sendgrid/dx-automator/actions/release@main 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Submit metric to Datadog 54 | uses: sendgrid/dx-automator/actions/datadog-release-metric@main 55 | env: 56 | DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} 57 | 58 | notify-on-failure: 59 | name: Slack notify on failure 60 | if: failure() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') 61 | needs: [ test, deploy ] 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: rtCamp/action-slack-notify@v2 65 | env: 66 | SLACK_COLOR: 'failure' 67 | SLACK_ICON_EMOJI: ':github:' 68 | 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) }} 69 | SLACK_TITLE: Action Failure - ${{ github.repository }} 70 | SLACK_USERNAME: GitHub Actions 71 | SLACK_MSG_AUTHOR: twilio-dx 72 | SLACK_FOOTER: Posted automatically using GitHub Actions 73 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 74 | MSG_MINIMAL: true 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | .DS_Store 14 | .env -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All the notable changes to this project will be documented in this file. 3 | 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | [2022-03-09] Version 0.6.9 7 | -------------------------- 8 | **Library - Chore** 9 | - [PR #91](https://github.com/sendgrid/smtpapi-go/pull/91): push Datadog Release Metric upon deploy success. Thanks to [@eshanholtz](https://github.com/eshanholtz)! 10 | 11 | 12 | [2022-02-09] Version 0.6.8 13 | -------------------------- 14 | **Library - Chore** 15 | - [PR #90](https://github.com/sendgrid/smtpapi-go/pull/90): upgrade supported language versions. Thanks to [@childish-sambino](https://github.com/childish-sambino)! 16 | - [PR #89](https://github.com/sendgrid/smtpapi-go/pull/89): add deploy job to test and deploy gh workflow. Thanks to [@Hunga1](https://github.com/Hunga1)! 17 | 18 | 19 | [2021-12-15] Version 0.6.7 20 | -------------------------- 21 | **Library - Chore** 22 | - [PR #88](https://github.com/sendgrid/smtpapi-go/pull/88): migrate to GitHub Actions. Thanks to [@beebzz](https://github.com/beebzz)! 23 | 24 | 25 | [2020-08-19] Version 0.6.6 26 | -------------------------- 27 | **Library - Chore** 28 | - [PR #86](https://github.com/sendgrid/smtpapi-go/pull/86): update GitHub branch references. Thanks to [@thinkingserious](https://github.com/thinkingserious)! 29 | 30 | **Library - Docs** 31 | - [PR #68](https://github.com/sendgrid/smtpapi-go/pull/68): Run *.md documents through Grammer.ly. Thanks to [@ssiddhantsharma](https://github.com/ssiddhantsharma)! 32 | 33 | 34 | [2020-08-05] Version 0.6.5 35 | -------------------------- 36 | **Library - Test** 37 | - [PR #56](https://github.com/sendgrid/smtpapi-go/pull/56): add CodeCov support to .travis.yml. Thanks to [@mptap](https://github.com/mptap)! 38 | 39 | 40 | [2020-07-08] Version 0.6.4 41 | -------------------------- 42 | **Library - Docs** 43 | - [PR #82](https://github.com/sendgrid/smtpapi-go/pull/82): add use cases directory. Thanks to [@mtroiani](https://github.com/mtroiani)! 44 | 45 | 46 | [2020-02-19] Version 0.6.3 47 | -------------------------- 48 | **Library - Docs** 49 | - [PR #61](https://github.com/sendgrid/smtpapi-go/pull/61): Added Code Review to Contributing.md. Thanks to [@mptap](https://github.com/mptap)! 50 | 51 | 52 | [2020-02-05] Version 0.6.2 53 | -------------------------- 54 | **Library - Docs** 55 | - [PR #77](https://github.com/sendgrid/smtpapi-go/pull/77): Fixed link to bug report template. Thanks to [@alxshelepenok](https://github.com/alxshelepenok)! 56 | 57 | 58 | [2020-01-30] Version 0.6.1 59 | -------------------------- 60 | **Library - Docs** 61 | - [PR #85](https://github.com/sendgrid/smtpapi-go/pull/85): baseline all the templated markdown docs. Thanks to [@childish-sambino](https://github.com/childish-sambino)! 62 | - [PR #80](https://github.com/sendgrid/smtpapi-go/pull/80): added announcement to README. Thanks to [@luciajimenez](https://github.com/luciajimenez)! 63 | 64 | **Library - Chore** 65 | - [PR #84](https://github.com/sendgrid/smtpapi-go/pull/84): prep repo for automation. Thanks to [@thinkingserious](https://github.com/thinkingserious)! 66 | - [PR #65](https://github.com/sendgrid/smtpapi-go/pull/65): fix failing build. Thanks to [@vaskoz](https://github.com/vaskoz)! 67 | - [PR #69](https://github.com/sendgrid/smtpapi-go/pull/69): add style standards check. Thanks to [@pangaunn](https://github.com/pangaunn)! 68 | 69 | 70 | [2018-06-04] Version 0.6.0 71 | -------------------------- 72 | ### Added 73 | - PR [#24](https://github.com/sendgrid/smtpapi-go/pull/24): Added Code of Conduct. Big thanks to [Shivam Agarwal](https://github.com/gr8shivam) for the PR! 74 | - PR [#26](https://github.com/sendgrid/smtpapi-go/pull/26): Added TROUBLESHOOTING.md. Big thanks to [Callam Delaney](https://github.com/cal97g) for the PR! 75 | - PR [#30](https://github.com/sendgrid/smtpapi-go/pull/30): SEO updates. Big thanks to [Eric Liu](https://github.com/eric1iu) for the PR! 76 | - PR [#32](https://github.com/sendgrid/smtpapi-go/pull/32): Added table of contents to README.md. Big thanks to [thepriefy](https://github.com/thepriefy) for the PR! 77 | - PR [#29](https://github.com/sendgrid/smtpapi-go/pull/29): Added README.md updates. Big thanks to [Andrew Hamilton](https://github.com/ahamilton55) for the PR! 78 | - PR [#36](https://github.com/sendgrid/smtpapi-go/pull/36): Added .github/ISSUE_TEMPLATE. Big thanks to [thepriefy](https://github.com/thepriefy) for the PR! 79 | - PR [#31](https://github.com/sendgrid/smtpapi-go/pull/31): Parallelize all tests. Big thanks to [Vasko Zdravevski](https://github.com/vaskoz) for the PR! 80 | - PR [#38](https://github.com/sendgrid/smtpapi-go/pull/38): Created PULL_REQUEST_TEMPLATE. Big thanks to [Anatoly](https://github.com/anatolyyyyyy) for the PR! 81 | - PR [#51](https://github.com/sendgrid/smtpapi-go/pull/51): Don't ignore erros in test. Big thanks to [Revolta](https://github.com/keydrevolta) for the PR! 82 | - PR [#52](https://github.com/sendgrid/smtpapi-go/pull/52): Include Gometalinter in the Travis CI build. Big thanks to [Vasko Zdravevski](https://github.com/vaskoz) for the PR! 83 | - PR [#42](https://github.com/sendgrid/smtpapi-go/pull/42): Added a .env_sample file, updated gitignore, updated README.md. Big thanks to [thepriefy](https://github.com/thepriefy) for the PR! 84 | - PR [#44](https://github.com/sendgrid/smtpapi-go/pull/44): Added Usage.md. Big thanks to [Tariq Ibrahim](https://github.com/tariq1890) for the PR! 85 | - PR [#49](https://github.com/sendgrid/smtpapi-go/pull/49): Added tests(License): The end year in the license file should be current year. Big thanks to [Marco Antônio Singer](https://github.com/marcosinger) for the PR! 86 | - PR [#53](https://github.com/sendgrid/smtpapi-go/pull/53): Added tests to check for the certain repo files to exist in the repo. Big thanks to [Gabriel Lima](https://github.com/gabrielclima) for the PR! 87 | - PR [#63](https://github.com/sendgrid/smtpapi-go/pull/63): Adding dynamic data support. Big thanks to [Ron Canada](https://github.com/roncanada) for the PR! 88 | 89 | ### Fixed 90 | - PR [#21](https://github.com/sendgrid/smtpapi-go/pull/21): README.md typo. Big thanks to [Cícero Pablo](https://github.com/ciceropablo) for the PR! 91 | - PR [#22](https://github.com/sendgrid/smtpapi-go/pull/22): Fixed golint issues, added gitignore and boosted the test coverage to 100%. Big thanks to [Tariq Ibrahim](https://github.com/tariq1890) for the PR! 92 | - PR [#33](https://github.com/sendgrid/smtpapi-go/pull/33): Using the right contributors badge for smtpapi-go. Big thanks to [Tariq Ibrahim](https://github.com/tariq1890) for the PR! 93 | - PR [#35](https://github.com/sendgrid/smtpapi-go/pull/35): Fixed the CONTRIBUTING.md typo. Big thanks to [Alex](https://github.com/pushkyn) for the PR! 94 | - PR [#48](https://github.com/sendgrid/smtpapi-go/pull/48): Update the LICENSE.txt year. Big thanks to [Alex](https://github.com/pushkyn) for the PR! 95 | 96 | ## [0.5.0] - 2017-06-13 97 | ### Added 98 | - PR #20: Support asm_groups_to_display 99 | - A big thanks to [Chris Lee](https://github.com/clee) for the PR! 100 | 101 | ## [0.4.2] - 2017-05-14 102 | ### Added 103 | - PR #19: Improved the code coverage 104 | - A big thanks to [Vasko Zdravevski](https://github.com/vaskoz) for the PR! 105 | 106 | ## [0.4.1] - 2016-11-15 107 | ### Fixed 108 | - PR #16: Add support for the non-string filter settings 109 | - A big thanks to [Stephen Young](https://github.com/hownowstephen) for the PR! 110 | 111 | ## [0.4.0] - 2015-7-19 112 | ### Added 113 | - IP Pools 114 | - Version constant 115 | 116 | ## [0.3.0] - 2015-04-09 117 | ### Added 118 | - Scheduling Parameters 119 | - CHANGELOG 120 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hello! Thank you for considering to help contribute to one of the SendGrid open source libraries. There are many ways you can contribute to your projects and additional help is always welcome. We simply require that you follow the following contribution policies. 2 | 3 | - [Improvements to the Codebase](#improvements-to-the-codebase) 4 | - [Understanding the Code Base](#understanding-the-codebase) 5 | - [Testing](#testing) 6 | - [Style Guidelines & Naming Conventions](#style-guidelines-and-naming-conventions) 7 | - [Creating a Pull Request](#creating-a-pull-request) 8 | - [Code Reviews](#code-reviews) 9 | 10 | 11 | ## Improvements to the Codebase 12 | 13 | We welcome direct contributions to our smtpapi-go code base. Thank you! 14 | 15 | ### Development Environment ### 16 | 17 | #### Install and Run Locally #### 18 | 19 | ##### Supported Versions ##### 20 | 21 | - Go 1.11-1.16 22 | 23 | ##### Initial setup: ##### 24 | 25 | ```bash 26 | git clone https://github.com/sendgrid/smtpapi-go.git 27 | cd smtpapi-go 28 | ``` 29 | 30 | ##### Execute: ##### 31 | 32 | See the [examples folder](examples) to get started quickly. 33 | 34 | To run the example: 35 | 36 | ```bash 37 | go run examples/example.go 38 | ``` 39 | 40 | 41 | ## Understanding the Code Base 42 | 43 | **/examples** 44 | 45 | Working examples that demonstrate usage. 46 | 47 | **/** 48 | 49 | *_test are the tests 50 | smtpapi.go is the source code 51 | 52 | 53 | ## Testing 54 | 55 | All PRs require passing tests before the PR will be reviewed. 56 | 57 | All test files are in the [`smtpapi_test.go`](smtpapi_test.go) file. 58 | 59 | For the purposes of contributing to this repo, please update the [`smtpapi_test.go`](smtpapi_test.go) and [`smtpapi_test_strings.json`](smtpapi_test_strings.json) files with unit tests as you modify the code. 60 | 61 | To run the tests: 62 | 63 | ```bash 64 | go test -v 65 | ``` 66 | 67 | 68 | ## Style Guidelines & Naming Conventions 69 | 70 | Generally, we follow the style guidelines as suggested by the official language. However, we ask that you conform to the styles that already exist in the library. If you wish to deviate, please explain your reasoning. 71 | 72 | - [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) 73 | 74 | Please run your code through: 75 | 76 | - [fmt](https://blog.golang.org/go-fmt-your-code) 77 | 78 | ## Creating a Pull Request 79 | 80 | 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your fork, 81 | and configure the remotes: 82 | 83 | ```bash 84 | # Clone your fork of the repo into the current directory 85 | git clone https://github.com/sendgrid/smtpapi-go 86 | # Navigate to the newly cloned directory 87 | cd smtpapi-go 88 | # Assign the original repo to a remote called "upstream" 89 | git remote add upstream https://github.com/sendgrid/smtpapi-go 90 | ``` 91 | 92 | 2. If you cloned this a while ago, get the latest changes from upstream: 93 | 94 | ```bash 95 | git checkout 96 | git pull upstream 97 | ``` 98 | 99 | 3. Create a new topic branch (off the main project development branch) to 100 | contain your feature, change, or fix: 101 | 102 | ```bash 103 | git checkout -b 104 | ``` 105 | 106 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 107 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 108 | or your code is unlikely be merged into the main project. Use Git's 109 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 110 | feature to tidy up your commits before making them public. 111 | 112 | 4a. Create tests. 113 | 114 | 4b. Create or update the example code that demonstrates the functionality of this change to the code. 115 | 116 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 117 | 118 | ```bash 119 | git pull [--rebase] upstream main 120 | ``` 121 | 122 | 6. Push your topic branch up to your fork: 123 | 124 | ```bash 125 | git push origin 126 | ``` 127 | 128 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 129 | with a clear title and description against the `main` branch. All tests must be passing before we will review the PR. 130 | 131 | 132 | ## Code Reviews 133 | 134 | 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/). 135 | -------------------------------------------------------------------------------- /FIRST_TIMERS.md: -------------------------------------------------------------------------------- 1 | # How To Contribute to Twilio SendGrid Repositories via GitHub 2 | Contributing to the Twilio SendGrid repositories is easy! All you need to do is find an open issue (see the bottom of this page for a list of repositories containing open issues), fix it and submit a pull request. Once you have submitted your pull request, the team can easily review it before it is merged into the repository. 3 | 4 | To make a pull request, follow these steps: 5 | 6 | 1. Log into GitHub. If you do not already have a GitHub account, you will have to create one in order to submit a change. Click the Sign up link in the upper right-hand corner to create an account. Enter your username, password, and email address. If you are an employee of Twilio SendGrid, please use your full name with your GitHub account and enter Twilio SendGrid as your company so we can easily identify you. 7 | 8 | 9 | 10 | 2. __[Fork](https://help.github.com/fork-a-repo/)__ the [smtpapi-go](https://github.com/sendgrid/smtpapi-go) repository: 11 | 12 | 13 | 14 | 3. __Clone__ your fork via the following commands: 15 | 16 | ```bash 17 | # Clone your fork of the repo into the current directory 18 | git clone https://github.com/your_username/smtpapi-go 19 | # Navigate to the newly cloned directory 20 | cd smtpapi-go 21 | # Assign the original repo to a remote called "upstream" 22 | git remote add upstream https://github.com/sendgrid/smtpapi-go 23 | ``` 24 | 25 | > Don't forget to replace *your_username* in the URL by your real GitHub username. 26 | 27 | 4. __Create a new topic branch__ (off the main project development branch) to contain your feature, change, or fix: 28 | 29 | ```bash 30 | git checkout -b 31 | ``` 32 | 33 | 5. __Commit your changes__ in logical chunks. 34 | 35 | Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up your commits before making them public. Probably you will also have to create tests (if needed) or create or update the example code that demonstrates the functionality of this change to the code. 36 | 37 | 6. __Locally merge (or rebase)__ the upstream development branch into your topic branch: 38 | 39 | ```bash 40 | git pull [--rebase] upstream main 41 | ``` 42 | 43 | 7. __Push__ your topic branch up to your fork: 44 | 45 | ```bash 46 | git push origin 47 | ``` 48 | 49 | 8. __[Open a Pull Request](https://help.github.com/articles/creating-a-pull-request/#changing-the-branch-range-and-destination-repository/)__ with a clear title and description against the `main` branch. All tests must be passing before we will review the PR. 50 | 51 | ## Important notice 52 | 53 | Before creating a pull request, make sure that you respect the repository's constraints regarding contributions. You can find them in the [CONTRIBUTING.md](CONTRIBUTING.md) file. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2023, Twilio SendGrid, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test install 2 | 3 | install: 4 | go get -t -v ./... 5 | 6 | test: install 7 | go test -race -cover -v ./... 8 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Fixes # 18 | 19 | A short description of what this PR does. 20 | 21 | ### Checklist 22 | - [x] I acknowledge that all my contributions will be made under the project's license 23 | - [ ] I have made a material change to the repo (functionality, testing, spelling, grammar) 24 | - [ ] I have read the [Contribution Guidelines](https://github.com/sendgrid/smtpapi-go/blob/main/CONTRIBUTING.md) and my PR follows them 25 | - [ ] I have titled the PR appropriately 26 | - [ ] I have updated my branch with the main branch 27 | - [ ] I have added tests that prove my fix is effective or that my feature works 28 | - [ ] I have added the necessary documentation about the functionality in the appropriate .md file 29 | - [ ] I have added inline documentation to the code I modified 30 | 31 | If you have questions, please file a [support ticket](https://support.sendgrid.com), or create a GitHub Issue in this repository. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SendGrid Logo](twilio_sendgrid_logo.png) 2 | 3 | [![BuildStatus](https://github.com/sendgrid/smtpapi-go/actions/workflows/test-and-deploy.yml/badge.svg)](https://github.com/sendgrid/smtpapi-go/actions/workflows/test-and-deploy.yml) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 5 | [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) 6 | [![GitHub contributors](https://img.shields.io/github/contributors/sendgrid/smtpapi-go.svg)](https://github.com/sendgrid/smtpapi-go/graphs/contributors) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/sendgrid/smtpapi-go)](https://goreportcard.com/report/github.com/sendgrid/smtpapi-go) 8 | [![GoDoc](https://godoc.org/github.com/sendgrid/smtpapi-go?status.svg)](https://godoc.org/github.com/sendgrid/smtpapi-go) 9 | 10 | **This is a simple library to simplify the process of using [SendGrid's](https://sendgrid.com) [X-SMTPAPI](http://sendgrid.com/docs/API_Reference/SMTP_API/index.html) with the Go programming language** 11 | 12 | # Table of Contents 13 | 14 | * [Announcements](#announcements) 15 | * [Installation](#installation) 16 | * [Quick Start](#quick-start) 17 | * [Usage](#usage) 18 | * [How to Contribute](#how-to-contribute) 19 | * [About](#about) 20 | * [Support](#support) 21 | * [License](#license) 22 | 23 | # Announcements 24 | All the updates to this library are documented in our [CHANGELOG](CHANGELOG.md). 25 | 26 | 27 | # Installation 28 | 29 | ## Supported Versions 30 | 31 | * Go version 1.11-1.17 32 | 33 | ## Prerequisites 34 | 35 | * The SendGrid service, starting at the [free level](https://sendgrid.com/free?source=smtpapi-go) 36 | 37 | ## Install the Package 38 | 39 | ```bash 40 | go get github.com/sendgrid/smtpapi-go 41 | ``` 42 | 43 | ## Setup the Environment Variables 44 | 45 | ### Environment Variable 46 | 47 | Update the development environment with your [SENDGRID_API_KEY](https://app.sendgrid.com/settings/api_keys), for example: 48 | 49 | ```bash 50 | echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env 51 | echo "sendgrid.env" >> .gitignore 52 | source ./sendgrid.env 53 | ``` 54 | 55 | 56 | # Quick Start 57 | 58 | ```go 59 | package main 60 | 61 | import ( 62 | "github.com/sendgrid/smtpapi-go" 63 | "fmt" 64 | ) 65 | 66 | func main() { 67 | header := smtpapi.NewSMTPAPIHeader() 68 | header.AddTo("test@example.com") 69 | fmt.Println(header.JSONString()) 70 | } 71 | ``` 72 | 73 | # Usage 74 | 75 | * [SendGrid Docs](https://sendgrid.com/docs/API_Reference/SMTP_API/index.html) 76 | * [Example Code](examples) 77 | 78 | # How to Contribute 79 | 80 | We encourage contribution to our libraries, please see our [CONTRIBUTING](CONTRIBUTING.md) guide for more details on contributions. 81 | 82 | Quick links: 83 | 84 | * [Feature Request](CONTRIBUTING.md#feature-request) 85 | * [Bug Reports](CONTRIBUTING.md#submit-a-bug-report) 86 | * [Improvements to the Codebase](CONTRIBUTING.md#improvements-to-the-codebase) 87 | * [Review Pull Requests](CONTRIBUTING.md#code-reviews) 88 | 89 | 90 | # About 91 | 92 | smtpapi-go is maintained and funded by Twilio SendGrid, Inc. The names and logos for smtpapi-go are trademarks of Twilio SendGrid, Inc. 93 | 94 | 95 | # Support 96 | 97 | If you need help with SendGrid, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com). 98 | 99 | 100 | # License 101 | 102 | [The MIT License (MIT)](LICENSE) 103 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | If you have a non-library SendGrid issue, please contact our [support team](https://support.sendgrid.com). 2 | 3 | If you can't find an issue below, please open an [issue](https://github.com/sendgrid/smtpapi-go/issues). 4 | 5 | 6 | ## Table of Contents 7 | * [Viewing the Request Body](#request-body) 8 | 9 | 10 | ## Viewing the request body 11 | 12 | If you are having issues with the SMTPAPI, or it is not working in the way you expect, 13 | viewing the instructions that are being sent by you, is a good place to start your troubleshooting. 14 | 15 | You can view the headers that we are passing to the SendGrid API in the following way. 16 | 17 | ```golang 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "github.com/sendgrid/smtpapi-go" 23 | ) 24 | 25 | func main(){ 26 | //instantiate NewSMTPAPIHeader 27 | header := smtapi.NewSMTPAPIHeader() 28 | 29 | //Set some value 30 | header.AddCategory("NewUser") 31 | 32 | fmt.Println(header.JSONString()) 33 | } 34 | ``` 35 | 36 | ```json 37 | {"category":["NewUser"]} 38 | ``` 39 | 40 | Now you can ensure that your headers will add up to your desired behaviour. 41 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Initialisation 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "github.com/sendgrid/smtpapi-go" 11 | ) 12 | 13 | header := smtpapi.NewSMTPAPIHeader() 14 | ``` 15 | 16 | ## Table of Contents 17 | 18 | - [Recipients](#recipients) 19 | - [Categories](#categories) 20 | - [Substitution](#substitution) 21 | - [Sections](#sections) 22 | - [Filters](#filters) 23 | - [Advanced Suppression Manager (ASM)](#asm) 24 | - [Send At](#send-at) 25 | - [Batches](#batches) 26 | - [IP Pools](#ip-pools) 27 | - [Retrieving Data](#retrieving-data) 28 | 29 | 30 | ## Recipients 31 | 32 | #### Adding a single recipient 33 | 34 | The `header.AddTo` method allows you to add a new recipient to the `To` array. 35 | 36 | To do this, you can use the function with an email address, and optionally include the recipient's name as a second parameter. 37 | 38 | ```go 39 | header.SetTo("example@example.com") // An email address with no name provided 40 | header.SetTo("example@example.com", "An Example User") // An email address with a name provided as the second parameter 41 | ``` 42 | 43 | #### Setting multiple recipients 44 | 45 | The `header.SetTos` method allows you to set multiple recipients by providing an array of email addresses. 46 | 47 | This will set the `To` array to the array you provide. This will need to either be an array of emails, or if names are provided, they need to be formatted as `{{ name }} <{{ email }}>`. 48 | 49 | ```go 50 | header.SetTos([]string{ 51 | "example@example.com", // An email address with no name 52 | "An Example User ", // An email with a name field 53 | }) 54 | ``` 55 | 56 | 57 | ## Categories 58 | 59 | Categories are a useful way to organise your email analytics by tagging your emails with a specific type or topic. 60 | 61 | There are multiple methods available for setting your categories when sending emails. 62 | 63 | #### Adding a single category 64 | 65 | The `header.AddCategory` method can be used to add a category to your list. 66 | 67 | ```go 68 | header.AddCategory('marketing') // Add a new category of 'marketing' to the array of categories 69 | ``` 70 | 71 | #### Setting multiple categories 72 | 73 | The `header.SetCategories` method can be used to set your categories list to an array of strings. 74 | 75 | This is useful when there are a set amount of categories required for the email you are sending. 76 | 77 | This method will remove any categories that have been previously set. 78 | 79 | ```go 80 | header.SetCategories([]string{ 81 | "marketing", 82 | "sales", 83 | }) // Sets the current categories to be 'marketing' and 'sales' 84 | ``` 85 | 86 | #### Setting a single category 87 | 88 | The `header.SetCategory` method can be used to set a single specific category. 89 | 90 | It is useful for removing the previously set categories and it will create a new array with the string you provide. 91 | 92 | This method will remove any categories that have been previously set. 93 | 94 | ```go 95 | header.SetCategory("marketing") // Reset the categories to be 'marketing' only 96 | ``` 97 | 98 | 99 | ## Substitution 100 | 101 | Substitutions are a great way of writing some short dynamic email content easily, 102 | 103 | #### Adding a single substitution string 104 | 105 | The `header.AddSubstitution` method can be used to replace the content for recipients. 106 | 107 | ```go 108 | header.AddSubstitution("-name-", "John") // Replace the -name- variable with John. 109 | ``` 110 | 111 | #### Setting substitution strings 112 | 113 | The `header.SetSubstitutions` method can be used to replace the content for any number of strings. 114 | 115 | This method will reset any key pairs that have previously been set. 116 | 117 | ```go 118 | header.SetSubstitutions(map[string][]string{ 119 | "-name-": {"John", "Jane"}, // Replace the -name- variable to John or Jane 120 | "-number-": {"555.555.5555", "777.777.7777"}, // Replace the -number- variable with the provided numbers 121 | }) 122 | ``` 123 | 124 | 125 | ## Sections 126 | 127 | Sections that are similar to substitutions, but are specific to the actual message rather than the recipient. 128 | 129 | This is useful when you are sending multiple emails with the same style, but with different content. 130 | 131 | Note that substitution variables can also be included within a section, but the section variables cannot. 132 | 133 | #### Adding a section 134 | 135 | The `header.AddSection` method can be used to add a new section to the sections array. This will be useful for building up a list of sections dynamically, perhaps based on a user's actions. 136 | 137 | ```go 138 | header.AddSection("-event_details-", "The event will be held tomorrow.") // Replaces -event_details- with the event's string 139 | ``` 140 | 141 | #### Setting multiple sections 142 | 143 | The `header.SetSections` allows you to set multiple sections in a single array. 144 | 145 | This is good when sending out multiple emails where there are no dynamic variations required. 146 | 147 | This will reset any section key-pairs that have previously been set. 148 | 149 | ```go 150 | header.SetSections(map[string]string{ 151 | "-event_details-": "The event will be held tomorrow.", 152 | "-event_open_time-": "'It will be open from 1am to 9pm.", 153 | }) 154 | ``` 155 | 156 | 157 | ## Filters 158 | 159 | Filters allow you to dynamically toggle features such as click tracking, blind copying and DKIM domain validation. 160 | 161 | #### Adding a single filter 162 | 163 | Adding a filter is easy by using the `header.AddFilter` method. 164 | 165 | This method requires 3 values: 166 | - The filter's name 167 | - The parameter's name 168 | - The value 169 | 170 | ```go 171 | header.AddFilter("dkim", "use_from", true) 172 | header.AddFilter("dkim", "domain", "example.com") 173 | ``` 174 | 175 | #### Adding pre-existing filters 176 | 177 | Filters with pre-determined settings can also be added using the `header.SetFilter` method. 178 | 179 | ```go 180 | filter := &Filter{ 181 | Settings: make(map[string]interface{}), 182 | } 183 | filter.Settings["enable"] = 1 184 | filter.Settings["text/plain"] = "You can haz footers!" 185 | header.SetFilter("footer", filter) 186 | ``` 187 | 188 | 189 | ## Advanced Suppression Management (ASM) 190 | 191 | Advanced Suppression Management (or Unsubscribe Groups) are a good way of allowing recipients to unsubscribe from a specific set of emails. 192 | 193 | You can 194 | 195 | #### Setting the ASM group ID 196 | 197 | The `header.SetASMGroupID` method is a quick way to set the type of email that you are sending. 198 | 199 | All it requires is the ID of the ASM group, which can be found using the API. 200 | 201 | ```go 202 | header.SetASMGroupID(42) // Sets the ASM ID to 42 203 | ``` 204 | 205 | 206 | ## Send At 207 | 208 | Scheduling the time of your email campaign can be done using a collection of quick and easy methods. 209 | 210 | #### Adding a single 'send at' date 211 | 212 | The `header.AddSendEachAt` method is a good way to add the time to send the email. 213 | 214 | This method requires a unix timestamp as the input. 215 | 216 | ```go 217 | header.AddSendEachAt(1508694645) 218 | ``` 219 | 220 | #### Setting multiple 'send at' date 221 | 222 | The `header.SetSendEachAt` method is a useful method for setting an array of times to which recipients have their emails sent. 223 | 224 | This method requires an array of unix timestamps as the input. 225 | 226 | ```go 227 | header.SetSendEachAt([]int64{ 228 | 1508694645, 229 | 1508694835, 230 | }) 231 | ``` 232 | 233 | #### Setting a single date to send all emails 234 | 235 | The `header.SetSendAt` method is useful for setting a single time that all emails in the collection will be sent. 236 | 237 | This method requires a unix timestamp as the input. 238 | 239 | ```go 240 | header.SetSendAt(1508694645) 241 | ``` 242 | 243 | 244 | ## Batches 245 | 246 | Batches are a great way to group a collection of scheduled items for sending. It allows you to cancel the scheduled emails, and provides more control over the emails. 247 | 248 | The batch ID can be set using the `header.AddBatchId` method. You must have generated the batch ID first with the help of the API. 249 | 250 | ```go 251 | header.AddBatchId("HkJ5yLYULb7Rj8GKSx7u025ouWVlMgAi") // Adds a previously generated batch ID to the emails 252 | ``` 253 | 254 | 255 | ## IP Pools 256 | 257 | IP Pools allow you to group SendGrid IP addresses together. For example, if you have a set of marketing IPs, you can assign them a pool ID of `marketing`. 258 | 259 | The IP Pool name can be set using the `header.SetIpPool` method. You must have generated the IP Pool first with the help of the API. 260 | 261 | ```go 262 | header.SetIpPool("marketing") // Sets the IP Pool to be the marketing collection of IPs 263 | ``` 264 | 265 | 266 | 267 | #### Retrieving data as a JSON string 268 | 269 | The `header.JSONString` method allows the data from the header instance to be exported as a JSON string. 270 | 271 | ```go 272 | headerString, _ := header.JSONString() 273 | fmt.Println(headerString) 274 | 275 | // {"to":["test@example.com"],"sub":{"key":["value"]},"section":{"section":"value"},"category":["category"],"unique_args":{"key":"value"},"filters":{"filter":{"settings":{"setting":"value"}}},"asm_group_id":1,"send_at":1428611024,"ip_pool":"testPool"} 276 | ``` -------------------------------------------------------------------------------- /examples/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sendgrid/smtpapi-go" 6 | ) 7 | 8 | func main() { 9 | header := smtpapi.NewSMTPAPIHeader() 10 | 11 | // Recipients 12 | header.AddTo("test@example.com") 13 | // or 14 | //tos := []string{"test1@example.com", "test2@example.com"} 15 | //header.AddTos(tos) 16 | // or 17 | //header.SetTos(tos) 18 | 19 | //[Substitutions](http://sendgrid.com/docs/API_Reference/SMTP_API/substitution_tags.html) 20 | header.AddSubstitution("key", "value") 21 | // or 22 | //values := []string{"value1", "value2"} 23 | //header.AddSubstitutions("key", values) 24 | // or 25 | //sub := make(map[string][]string) 26 | //sub["key"] = values 27 | //header.SetSubstitutions(sub) 28 | 29 | //[Section](http://sendgrid.com/docs/API_Reference/SMTP_API/section_tags.html) 30 | header.AddSection("section", "value") 31 | // or 32 | //sections := make(map[string]string) 33 | //sections["section"] = "value" 34 | //header.SetSections(sections) 35 | 36 | //[Category](http://sendgrid.com/docs/Delivery_Metrics/categories.html) 37 | header.AddCategory("category") 38 | // or 39 | //categories := []string{"setCategories"} 40 | //header.AddCategories(categories) 41 | // or 42 | //header.SetCategories(categories) 43 | 44 | //[Unique Arguments](http://sendgrid.com/docs/API_Reference/SMTP_API/unique_arguments.html) 45 | header.AddUniqueArg("key", "value") 46 | // or 47 | //args := make(map[string]string) 48 | //args["key"] = "value" 49 | //header.SetUniqueArgs(args) 50 | 51 | //[Filters](http://sendgrid.com/docs/API_Reference/SMTP_API/apps.html) 52 | header.AddFilter("filter", "setting", "value") 53 | // or 54 | /* 55 | filter := &Filter{ 56 | Settings: make(map[string]string), 57 | } 58 | filter.Settings["enable"] = "1" 59 | filter.Settings["text/plain"] = "You can haz footers!" 60 | header.SetFilter("footer", filter) 61 | */ 62 | 63 | //[Send At](https://sendgrid.com/docs/API_Reference/SMTP_API/scheduling_parameters.html) 64 | header.SetSendAt(1428611024) 65 | // or 66 | //sendEachAt := []int64{1428611024, 1428611025} 67 | //header.SetSendEachAt(sendEachAt) 68 | // or 69 | //header.AddSendEachAt(1428611024) 70 | //header.AddSendEachAt(1428611025) 71 | 72 | //[ASM Group ID](https://sendgrid.com/docs/User_Guide/advanced_suppression_manager.html) 73 | asmGroupID := 1 74 | header.SetASMGroupID(asmGroupID) 75 | 76 | // [IP Pools](https://sendgrid.com/docs/API_Reference/Web_API_v3/IP_Management/ip_pools.html) 77 | header.SetIpPool("testPool") 78 | 79 | fmt.Println(header.JSONString()) 80 | } 81 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -race -coverprofile=profile.out -covermode=atomic $d 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done 13 | -------------------------------------------------------------------------------- /smtpapi.go: -------------------------------------------------------------------------------- 1 | package smtpapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "unicode/utf16" 8 | ) 9 | 10 | // Version represents the current version of the smtpapi-go library 11 | const Version = "0.6.9" 12 | 13 | // SMTPAPIHeader will be used to set up X-SMTPAPI params 14 | type SMTPAPIHeader struct { 15 | To []string `json:"to,omitempty"` 16 | Sub map[string][]string `json:"sub,omitempty"` 17 | Section map[string]string `json:"section,omitempty"` 18 | Category []string `json:"category,omitempty"` 19 | UniqueArgs map[string]string `json:"unique_args,omitempty"` 20 | Filters map[string]Filter `json:"filters,omitempty"` 21 | ASMGroupID int `json:"asm_group_id,omitempty"` 22 | ASMGroups []int `json:"asm_groups_to_display,omitempty"` 23 | SendAt int64 `json:"send_at,omitempty"` 24 | SendEachAt []int64 `json:"send_each_at,omitempty"` 25 | IpPool string `json:"ip_pool,omitempty"` 26 | BatchID string `json:"batch_id,omitempty"` 27 | DynamicData map[string]interface{} `json:"dynamic_template_data,omitempty"` 28 | } 29 | 30 | // Filter represents an App/Filter and its settings 31 | type Filter struct { 32 | Settings map[string]interface{} `json:"settings,omitempty"` 33 | } 34 | 35 | // NewSMTPAPIHeader creates a new header struct 36 | func NewSMTPAPIHeader() *SMTPAPIHeader { 37 | return &SMTPAPIHeader{} 38 | } 39 | 40 | // AddTo appends a single email to the To header 41 | func (h *SMTPAPIHeader) AddTo(email string) { 42 | h.To = append(h.To, email) 43 | } 44 | 45 | // AddTos appends multiple emails to the To header 46 | func (h *SMTPAPIHeader) AddTos(emails []string) { 47 | for i := 0; i < len(emails); i++ { 48 | h.AddTo(emails[i]) 49 | } 50 | } 51 | 52 | // SetTos sets the value of the To header 53 | func (h *SMTPAPIHeader) SetTos(emails []string) { 54 | h.To = emails 55 | } 56 | 57 | // AddSubstitution adds a new substitution to a specific key 58 | func (h *SMTPAPIHeader) AddSubstitution(key, sub string) { 59 | if h.Sub == nil { 60 | h.Sub = make(map[string][]string) 61 | } 62 | h.Sub[key] = append(h.Sub[key], sub) 63 | } 64 | 65 | // AddSubstitutions adds a multiple substitutions to a specific key 66 | func (h *SMTPAPIHeader) AddSubstitutions(key string, subs []string) { 67 | for i := 0; i < len(subs); i++ { 68 | h.AddSubstitution(key, subs[i]) 69 | } 70 | } 71 | 72 | // SetSubstitutions sets the value of the substitutions on the Sub header 73 | func (h *SMTPAPIHeader) SetSubstitutions(sub map[string][]string) { 74 | h.Sub = sub 75 | } 76 | 77 | // AddSection sets the value for a specific section 78 | func (h *SMTPAPIHeader) AddSection(section, value string) { 79 | if h.Section == nil { 80 | h.Section = make(map[string]string) 81 | } 82 | h.Section[section] = value 83 | } 84 | 85 | // SetSections sets the value for the Section header 86 | func (h *SMTPAPIHeader) SetSections(sections map[string]string) { 87 | h.Section = sections 88 | } 89 | 90 | // AddCategory adds a new category to the Category header 91 | func (h *SMTPAPIHeader) AddCategory(category string) { 92 | h.Category = append(h.Category, category) 93 | } 94 | 95 | // AddCategories adds multiple categories to the Category header 96 | func (h *SMTPAPIHeader) AddCategories(categories []string) { 97 | for i := 0; i < len(categories); i++ { 98 | h.AddCategory(categories[i]) 99 | } 100 | } 101 | 102 | // SetCategories will set the value of the Categories field 103 | func (h *SMTPAPIHeader) SetCategories(categories []string) { 104 | h.Category = categories 105 | } 106 | 107 | // SetASMGroupID will set the value of the ASMGroupID field 108 | func (h *SMTPAPIHeader) SetASMGroupID(groupID int) { 109 | h.ASMGroupID = groupID 110 | } 111 | 112 | // AddASMGroupToDisplay adds a new ASM group ID to be displayed 113 | func (h *SMTPAPIHeader) AddASMGroupToDisplay(groupID int) { 114 | h.ASMGroups = append(h.ASMGroups, groupID) 115 | } 116 | 117 | // AddASMGroupsToDisplay adds multiple ASM group IDs to be displayed 118 | func (h *SMTPAPIHeader) AddASMGroupsToDisplay(groupIDs []int) { 119 | for i := 0; i < len(groupIDs); i++ { 120 | h.AddASMGroupToDisplay(groupIDs[i]) 121 | } 122 | } 123 | 124 | // SetASMGroupsToDisplay will set the value of the ASMGroups field 125 | func (h *SMTPAPIHeader) SetASMGroupsToDisplay(groups []int) { 126 | h.ASMGroups = groups 127 | } 128 | 129 | // AddUniqueArg will set the value of a specific argument 130 | func (h *SMTPAPIHeader) AddUniqueArg(arg, value string) { 131 | if h.UniqueArgs == nil { 132 | h.UniqueArgs = make(map[string]string) 133 | } 134 | h.UniqueArgs[arg] = value 135 | } 136 | 137 | // SetUniqueArgs will set the value of the Unique_args header 138 | func (h *SMTPAPIHeader) SetUniqueArgs(args map[string]string) { 139 | h.UniqueArgs = args 140 | } 141 | 142 | // AddFilter will set the specific setting for a filter 143 | func (h *SMTPAPIHeader) AddFilter(filter, setting string, value interface{}) { 144 | if h.Filters == nil { 145 | h.Filters = make(map[string]Filter) 146 | } 147 | if _, ok := h.Filters[filter]; !ok { 148 | h.Filters[filter] = Filter{ 149 | Settings: make(map[string]interface{}), 150 | } 151 | } 152 | h.Filters[filter].Settings[setting] = value 153 | } 154 | 155 | // SetFilter takes in a Filter struct with predetermined settings and sets it for such Filter key 156 | func (h *SMTPAPIHeader) SetFilter(filter string, value *Filter) { 157 | if h.Filters == nil { 158 | h.Filters = make(map[string]Filter) 159 | } 160 | h.Filters[filter] = *value 161 | } 162 | 163 | // SetSendAt takes in a timestamp which determines when the email will be sent 164 | func (h *SMTPAPIHeader) SetSendAt(sendAt int64) { 165 | h.SendAt = sendAt 166 | } 167 | 168 | // AddSendEachAt takes in a timestamp and pushes it into a list Must match length of To emails 169 | func (h *SMTPAPIHeader) AddSendEachAt(sendEachAt int64) { 170 | h.SendEachAt = append(h.SendEachAt, sendEachAt) 171 | } 172 | 173 | // SetSendEachAt takes an array of timestamps. Must match length of To emails 174 | func (h *SMTPAPIHeader) SetSendEachAt(sendEachAt []int64) { 175 | h.SendEachAt = sendEachAt 176 | } 177 | 178 | // SetIpPool takes a strings and sets the IpPool field 179 | func (h *SMTPAPIHeader) SetIpPool(ipPool string) { 180 | h.IpPool = ipPool 181 | } 182 | 183 | // Unicode escape 184 | func escapeUnicode(input string) string { 185 | //var buffer bytes.Buffer 186 | buffer := bytes.NewBufferString("") 187 | for _, r := range input { 188 | if r > 65535 { 189 | // surrogate pair 190 | var r1, r2 = utf16.EncodeRune(r) 191 | var s = fmt.Sprintf("\\u%x\\u%x", r1, r2) 192 | // error always nil https://golang.org/pkg/bytes/#Buffer.WriteString 193 | buffer.WriteString(s) // nolint: gas, gosec 194 | } else if r > 127 { 195 | var s = fmt.Sprintf("\\u%04x", r) 196 | // error always nil https://golang.org/pkg/bytes/#Buffer.WriteString 197 | buffer.WriteString(s) // nolint: gas, gosec 198 | } else { 199 | var s = fmt.Sprintf("%c", r) 200 | // error always nil https://golang.org/pkg/bytes/#Buffer.WriteString 201 | buffer.WriteString(s) // nolint: gas, gosec 202 | } 203 | } 204 | return buffer.String() 205 | } 206 | 207 | // JSONString returns the representation of the Header 208 | func (h *SMTPAPIHeader) JSONString() (string, error) { 209 | headers, e := json.Marshal(h) 210 | return escapeUnicode(string(headers)), e 211 | } 212 | 213 | // Load allows you to load a pre-formed x-smtpapi header 214 | func (h *SMTPAPIHeader) Load(b []byte) error { 215 | return json.Unmarshal(b, h) 216 | } 217 | -------------------------------------------------------------------------------- /smtpapi_test.go: -------------------------------------------------------------------------------- 1 | package smtpapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "reflect" 9 | "regexp" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func exampleJson() map[string]interface{} { 15 | data, _ := ioutil.ReadFile("smtpapi_test_strings.json") 16 | var f interface{} 17 | json.Unmarshal(data, &f) 18 | json := f.(map[string]interface{}) 19 | return json 20 | } 21 | 22 | func TestNewSMTPIAPIHeader(t *testing.T) { 23 | t.Parallel() 24 | header := NewSMTPAPIHeader() 25 | if header == nil { 26 | t.Error("NewSMTPAPIHeader() should never return nil") 27 | } 28 | } 29 | 30 | func TestAddTo(t *testing.T) { 31 | t.Parallel() 32 | header := NewSMTPAPIHeader() 33 | header.AddTo("addTo@mailinator.com") 34 | result, _ := header.JSONString() 35 | if result != exampleJson()["add_to"] { 36 | t.Errorf("Result did not match") 37 | } 38 | } 39 | 40 | func TestAddTos(t *testing.T) { 41 | t.Parallel() 42 | header := NewSMTPAPIHeader() 43 | tos := []string{"addTo@mailinator.com"} 44 | header.AddTos(tos) 45 | result, _ := header.JSONString() 46 | if result != exampleJson()["add_to"] { 47 | t.Errorf("Result did not match") 48 | } 49 | } 50 | 51 | func TestSetTos(t *testing.T) { 52 | t.Parallel() 53 | header := NewSMTPAPIHeader() 54 | header.SetTos([]string{"setTos@mailinator.com"}) 55 | result, _ := header.JSONString() 56 | if result != exampleJson()["set_tos"] { 57 | t.Errorf("Result did not match") 58 | } 59 | } 60 | 61 | func TestAddSubstitution(t *testing.T) { 62 | t.Parallel() 63 | header := NewSMTPAPIHeader() 64 | header.AddSubstitution("sub", "val") 65 | result, _ := header.JSONString() 66 | if result != exampleJson()["add_substitution"] { 67 | t.Errorf("Result did not match") 68 | } 69 | } 70 | 71 | func TestAddSubstitutions(t *testing.T) { 72 | t.Parallel() 73 | header := NewSMTPAPIHeader() 74 | header.AddSubstitutions("sub", []string{"val"}) 75 | result, _ := header.JSONString() 76 | if result != exampleJson()["add_substitution"] { 77 | t.Errorf("Result did not match") 78 | } 79 | } 80 | 81 | func TestSetSubstitutions(t *testing.T) { 82 | t.Parallel() 83 | header := NewSMTPAPIHeader() 84 | sub := make(map[string][]string) 85 | sub["sub"] = []string{"val"} 86 | header.SetSubstitutions(sub) 87 | result, _ := header.JSONString() 88 | if result != exampleJson()["set_substitutions"] { 89 | t.Errorf("Result did not match") 90 | } 91 | } 92 | 93 | func TestAddSection(t *testing.T) { 94 | t.Parallel() 95 | header := NewSMTPAPIHeader() 96 | header.AddSection("set_section_key", "set_section_value") 97 | header.AddSection("set_section_key_2", "set_section_value_2") 98 | result, _ := header.JSONString() 99 | if result != exampleJson()["add_section"] { 100 | t.Errorf("Result did not match") 101 | } 102 | } 103 | 104 | func TestSetSections(t *testing.T) { 105 | t.Parallel() 106 | header := NewSMTPAPIHeader() 107 | sections := make(map[string]string) 108 | sections["set_section_key"] = "set_section_value" 109 | header.SetSections(sections) 110 | result, _ := header.JSONString() 111 | if result != exampleJson()["set_sections"] { 112 | t.Errorf("Result did not match") 113 | } 114 | } 115 | 116 | func TestAddCategory(t *testing.T) { 117 | t.Parallel() 118 | header := NewSMTPAPIHeader() 119 | header.AddCategory("addCategory") 120 | header.AddCategory("addCategory2") 121 | result, _ := header.JSONString() 122 | if result != exampleJson()["add_category"] { 123 | t.Errorf("Result did not match") 124 | } 125 | } 126 | 127 | func TestAddCategoryUnicode(t *testing.T) { 128 | t.Parallel() 129 | header := NewSMTPAPIHeader() 130 | header.AddCategory("カテゴリUnicode") 131 | header.AddCategory("カテゴリ2Unicode") 132 | header.AddCategory("鼖") 133 | result, _ := header.JSONString() 134 | if result != exampleJson()["add_category_unicode"] { 135 | t.Errorf("Result did not match") 136 | } 137 | } 138 | 139 | func TestAddCategories(t *testing.T) { 140 | t.Parallel() 141 | header := NewSMTPAPIHeader() 142 | categories := []string{"addCategory", "addCategory2"} 143 | header.AddCategories(categories) 144 | result, _ := header.JSONString() 145 | if result != exampleJson()["add_category"] { 146 | t.Errorf("Result did not match") 147 | } 148 | } 149 | 150 | func TestSetCategories(t *testing.T) { 151 | t.Parallel() 152 | header := NewSMTPAPIHeader() 153 | header.SetCategories([]string{"setCategories"}) 154 | result, _ := header.JSONString() 155 | if result != exampleJson()["set_categories"] { 156 | t.Errorf("Result did not match") 157 | } 158 | } 159 | 160 | func TestAddUniqueArg(t *testing.T) { 161 | t.Parallel() 162 | header := NewSMTPAPIHeader() 163 | header.AddUniqueArg("add_unique_argument_key", "add_unique_argument_value") 164 | header.AddUniqueArg("add_unique_argument_key_2", "add_unique_argument_value_2") 165 | result, _ := header.JSONString() 166 | if result != exampleJson()["add_unique_arg"] { 167 | t.Errorf("Result did not match") 168 | } 169 | } 170 | 171 | func TestSetUniqueArgs(t *testing.T) { 172 | t.Parallel() 173 | header := NewSMTPAPIHeader() 174 | args := make(map[string]string) 175 | args["set_unique_argument_key"] = "set_unique_argument_value" 176 | header.SetUniqueArgs(args) 177 | result, _ := header.JSONString() 178 | if result != exampleJson()["set_unique_args"] { 179 | t.Errorf("Result did not match") 180 | } 181 | } 182 | 183 | func TestAddFilter(t *testing.T) { 184 | t.Parallel() 185 | header := NewSMTPAPIHeader() 186 | header.AddFilter("footer", "text/html", "boo") 187 | if len(header.Filters) != 1 { 188 | t.Error("AddFilter failed") 189 | } 190 | } 191 | 192 | func TestSetFilter(t *testing.T) { 193 | t.Parallel() 194 | header := NewSMTPAPIHeader() 195 | filter := &Filter{ 196 | Settings: make(map[string]interface{}), 197 | } 198 | filter.Settings["enable"] = 1 199 | filter.Settings["text/plain"] = "You can haz footers!" 200 | header.SetFilter("footer", filter) 201 | result, _ := header.JSONString() 202 | if result != exampleJson()["set_filters"] { 203 | t.Errorf("Result did not match") 204 | } 205 | } 206 | 207 | func TestSetSendAt(t *testing.T) { 208 | t.Parallel() 209 | header := NewSMTPAPIHeader() 210 | header.SetSendAt(1428611024) 211 | result, _ := header.JSONString() 212 | if result != exampleJson()["set_send_at"] { 213 | t.Errorf("Result did not match") 214 | } 215 | } 216 | 217 | func TestAddSendEachAt(t *testing.T) { 218 | t.Parallel() 219 | header := NewSMTPAPIHeader() 220 | header.AddSendEachAt(1428611024) 221 | header.AddSendEachAt(1428611025) 222 | result, _ := header.JSONString() 223 | if result != exampleJson()["add_send_each_at"] { 224 | t.Errorf("Result did not match") 225 | } 226 | } 227 | 228 | func TestSetSendEachAt(t *testing.T) { 229 | t.Parallel() 230 | header := NewSMTPAPIHeader() 231 | sendEachAt := []int64{1428611024, 1428611025} 232 | header.SetSendEachAt(sendEachAt) 233 | result, _ := header.JSONString() 234 | if result != exampleJson()["set_send_each_at"] { 235 | t.Errorf("Result did not match") 236 | } 237 | } 238 | 239 | func TestSetASMGroupID(t *testing.T) { 240 | t.Parallel() 241 | header := NewSMTPAPIHeader() 242 | header.SetASMGroupID(1) 243 | result, _ := header.JSONString() 244 | if result != exampleJson()["set_asm_group_id"] { 245 | t.Errorf("Result did not match") 246 | } 247 | } 248 | 249 | func TestSetIpPool(t *testing.T) { 250 | t.Parallel() 251 | header := NewSMTPAPIHeader() 252 | header.SetIpPool("testPool") 253 | result, _ := header.JSONString() 254 | if result != exampleJson()["set_ip_pool"] { 255 | t.Errorf("Result did not match") 256 | } 257 | } 258 | 259 | func TestSAddASMGroupToDisplay(t *testing.T) { 260 | t.Parallel() 261 | header := NewSMTPAPIHeader() 262 | header.AddASMGroupToDisplay(671332) 263 | result, _ := header.JSONString() 264 | if result != exampleJson()["add_asm_group"] { 265 | t.Errorf("Result did not match") 266 | } 267 | } 268 | 269 | func TestSAddASMGroupsToDisplay(t *testing.T) { 270 | t.Parallel() 271 | header := NewSMTPAPIHeader() 272 | header.AddASMGroupsToDisplay([]int{45, 23}) 273 | result, _ := header.JSONString() 274 | if result != exampleJson()["add_asm_groups"] { 275 | t.Errorf("Result did not match") 276 | } 277 | } 278 | 279 | func TestJSONString(t *testing.T) { 280 | t.Parallel() 281 | header := NewSMTPAPIHeader() 282 | result, _ := header.JSONString() 283 | if result != exampleJson()["json_string"] { 284 | t.Errorf("Result did not match") 285 | } 286 | } 287 | 288 | func TestJSONStringWithAdds(t *testing.T) { 289 | t.Parallel() 290 | validHeader, _ := json.Marshal([]byte(`{"to":["test@email.com"],"sub":{"subKey":["subValue"]},"section":{"testSection":"sectionValue"},"category":["testCategory"],"unique_args":{"testUnique":"uniqueValue"},"filters":{"testFilter":{"settings":{"filter":"filterValue"}}}}`)) 291 | header := NewSMTPAPIHeader() 292 | header.AddTo("test@email.com") 293 | header.AddSubstitution("subKey", "subValue") 294 | header.AddSection("testSection", "sectionValue") 295 | header.AddCategory("testCategory") 296 | header.AddUniqueArg("testUnique", "uniqueValue") 297 | header.AddFilter("testFilter", "filter", "filterValue") 298 | if h, e := header.JSONString(); e != nil { 299 | t.Errorf("Error! %s", e) 300 | } else { 301 | testHeader, _ := json.Marshal([]byte(h)) 302 | if reflect.DeepEqual(testHeader, validHeader) { 303 | t.Logf("Success") 304 | } else { 305 | t.Errorf("Invalid header") 306 | } 307 | } 308 | } 309 | 310 | func TestJSONStringWithSets(t *testing.T) { 311 | t.Parallel() 312 | validHeader, _ := json.Marshal([]byte(`{"to":["test@email.com"],"sub":{"subKey":["subValue"]},"section":{"testSection":"sectionValue"},"category":["testCategory"],"unique_args":{"testUnique":"uniqueValue"},"filters":{"testFilter":{"settings":{"filter":"filterValue"}}},"asm_group_id":1,"ip_pool":"testPool"}`)) 313 | header := NewSMTPAPIHeader() 314 | header.SetTos([]string{"test@email.com"}) 315 | sub := make(map[string][]string) 316 | sub["subKey"] = []string{"subValue"} 317 | header.SetSubstitutions(sub) 318 | sections := make(map[string]string) 319 | sections["testSection"] = "sectionValue" 320 | header.SetSections(sections) 321 | header.SetCategories([]string{"testCategory"}) 322 | unique := make(map[string]string) 323 | unique["testUnique"] = "uniqueValue" 324 | header.SetUniqueArgs(unique) 325 | header.AddFilter("testFilter", "filter", "filterValue") 326 | header.SetASMGroupID(1) 327 | header.SetIpPool("testPool") 328 | if h, e := header.JSONString(); e != nil { 329 | t.Errorf("Error! %s", e) 330 | } else { 331 | testHeader, _ := json.Marshal([]byte(h)) 332 | if reflect.DeepEqual(testHeader, validHeader) { 333 | t.Logf("Success") 334 | } else { 335 | t.Errorf("Invalid header") 336 | } 337 | } 338 | } 339 | 340 | func TestMarshalUnmarshall(t *testing.T) { 341 | t.Parallel() 342 | header := NewSMTPAPIHeader() 343 | header.SetTos([]string{"test@email.com"}) 344 | sub := make(map[string][]string) 345 | sub["subKey"] = []string{"subValue"} 346 | header.SetSubstitutions(sub) 347 | sections := make(map[string]string) 348 | sections["testSection"] = "sectionValue" 349 | header.SetSections(sections) 350 | header.SetCategories([]string{"testCategory"}) 351 | unique := make(map[string]string) 352 | unique["testUnique"] = "uniqueValue" 353 | header.SetUniqueArgs(unique) 354 | header.AddFilter("testFilter", "filter", "filterValue") 355 | header.SetASMGroupID(1) 356 | header.SetIpPool("testPool") 357 | header.SetASMGroupsToDisplay([]int{32, 12}) 358 | 359 | newHeader := NewSMTPAPIHeader() 360 | b, err := header.JSONString() 361 | if err != nil { 362 | t.Errorf("Error in JSONString %v", err) 363 | } 364 | err = newHeader.Load([]byte(b)) 365 | if err != nil { 366 | t.Errorf("Could not load newHeader %v", err) 367 | } 368 | if !reflect.DeepEqual(header, newHeader) { 369 | t.Errorf("Expected %v, but got %v", header, newHeader) 370 | } 371 | } 372 | 373 | func TestRepoFiles(t *testing.T) { 374 | files := []string{".env_sample", ".gitignore", ".github/workflows/test-and-deploy.yml", "CHANGELOG.md", "CODE_OF_CONDUCT.md", 375 | "CONTRIBUTING.md", "LICENSE", "PULL_REQUEST_TEMPLATE.md", 376 | "README.md", "TROUBLESHOOTING.md", "USAGE.md"} 377 | 378 | for _, file := range files { 379 | if _, err := os.Stat(file); os.IsNotExist(err) { 380 | t.Errorf("Repo file does not exist: %v", file) 381 | } 382 | } 383 | } 384 | 385 | func TestLicenseYear(t *testing.T) { 386 | t.Parallel() 387 | dat, err := ioutil.ReadFile("LICENSE") 388 | 389 | currentYear := time.Now().Year() 390 | r := fmt.Sprintf("%d", currentYear) 391 | match, _ := regexp.MatchString(r, string(dat)) 392 | 393 | if err != nil { 394 | t.Error("License File Not Found") 395 | } 396 | if !match { 397 | t.Error("Incorrect Year in License Copyright") 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /smtpapi_test_strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_string": "{}", 3 | "add_to": "{\"to\":[\"addTo@mailinator.com\"]}", 4 | "set_tos": "{\"to\":[\"setTos@mailinator.com\"]}", 5 | "add_substitution": "{\"sub\":{\"sub\":[\"val\"]}}", 6 | "set_substitutions": "{\"sub\":{\"sub\":[\"val\"]}}", 7 | "add_unique_arg": "{\"unique_args\":{\"add_unique_argument_key\":\"add_unique_argument_value\",\"add_unique_argument_key_2\":\"add_unique_argument_value_2\"}}", 8 | "set_unique_args": "{\"unique_args\":{\"set_unique_argument_key\":\"set_unique_argument_value\"}}", 9 | "add_category": "{\"category\":[\"addCategory\",\"addCategory2\"]}", 10 | "add_category_unicode": "{\"category\":[\"\\u30ab\\u30c6\\u30b4\\u30eaUnicode\",\"\\u30ab\\u30c6\\u30b4\\u30ea2Unicode\",\"\\ud87e\\ude1b\"]}", 11 | "set_categories": "{\"category\":[\"setCategories\"]}", 12 | "add_section": "{\"section\":{\"set_section_key\":\"set_section_value\",\"set_section_key_2\":\"set_section_value_2\"}}", 13 | "set_sections": "{\"section\":{\"set_section_key\":\"set_section_value\"}}", 14 | "add_filter": "{\"filters\":{\"footer\":{\"settings\":{\"text/html\":\"boo\"}}}}", 15 | "set_filters": "{\"filters\":{\"footer\":{\"settings\":{\"enable\":1,\"text/plain\":\"You can haz footers!\"}}}}", 16 | "set_send_at": "{\"send_at\":1428611024}", 17 | "add_send_each_at": "{\"send_each_at\":[1428611024,1428611025]}", 18 | "set_send_each_at": "{\"send_each_at\":[1428611024,1428611025]}", 19 | "set_asm_group_id": "{\"asm_group_id\":1}", 20 | "add_asm_group": "{\"asm_groups_to_display\":[671332]}", 21 | "add_asm_groups": "{\"asm_groups_to_display\":[45,23]}", 22 | "set_ip_pool": "{\"ip_pool\":\"testPool\"}" 23 | } 24 | -------------------------------------------------------------------------------- /static/img/github-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-go/2be719257fe040c6b129028f2b9aa6db7612f679/static/img/github-fork.png -------------------------------------------------------------------------------- /static/img/github-sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-go/2be719257fe040c6b129028f2b9aa6db7612f679/static/img/github-sign-up.png -------------------------------------------------------------------------------- /twilio_sendgrid_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/smtpapi-go/2be719257fe040c6b129028f2b9aa6db7612f679/twilio_sendgrid_logo.png -------------------------------------------------------------------------------- /use_cases/README.md: -------------------------------------------------------------------------------- 1 | This directory provides examples for specific use cases. Please [open an issue](https://github.com/sendgrid/smtpapi-go/issues) or make a pull request for any use cases you would like to see here. Thank you! 2 | 3 | # Table of Contents 4 | --------------------------------------------------------------------------------