Repository: ainsleyclark/go-mail
Branch: main
Commit: 1c0a1e6ec5da
Files: 81
Total size: 167.5 KB
Directory structure:
gitextract_8hbv64jc/
├── .editorconfig
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SECURITY.md
│ └── workflows/
│ ├── codeql-analysis.yml
│ ├── email.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── bin/
│ ├── tag.sh
│ └── tests.sh
├── drivers/
│ ├── drivers.go
│ ├── drivers_test.go
│ ├── mailgun.go
│ ├── mailgun_test.go
│ ├── postal.go
│ ├── postal_test.go
│ ├── postmark.go
│ ├── postmark_test.go
│ ├── sendgrid.go
│ ├── sendgrid_test.go
│ ├── smtp.go
│ ├── smtp_test.go
│ ├── sparkpost.go
│ └── sparkpost_test.go
├── examples/
│ ├── attachments.go
│ ├── mailgun.go
│ ├── postal.go
│ ├── postmark.go
│ ├── sendgrid.go
│ ├── smtp.go
│ └── sparkpost.go
├── go.mod
├── go.sum
├── internal/
│ ├── client/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── util.go
│ │ └── util_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ ├── httputil/
│ │ ├── payload.go
│ │ ├── payload_test.go
│ │ ├── request.go
│ │ ├── request_test.go
│ │ └── response.go
│ ├── mime/
│ │ ├── mime.go
│ │ └── mime_test.go
│ └── mocks/
│ ├── client/
│ │ └── Requester.go
│ ├── drivers/
│ │ └── smtpSendFunc.go
│ ├── httputil/
│ │ ├── Payload.go
│ │ └── Responder.go
│ └── mail/
│ └── Mailer.go
├── mail/
│ ├── attachments.go
│ ├── attachments_test.go
│ ├── config.go
│ ├── config_test.go
│ ├── mail.go
│ ├── mail_test.go
│ ├── response.go
│ ├── transmissions.go
│ └── transmissions_test.go
├── mocks/
│ ├── client/
│ │ └── Requester.go
│ ├── clientold/
│ │ └── Requester.go
│ ├── drivers/
│ │ └── smtpSendFunc.go
│ ├── httputil/
│ │ ├── Payload.go
│ │ └── Responder.go
│ └── mail/
│ └── Mailer.go
└── tests/
├── mail_test.go
├── mailgun_test.go
├── postal_test.go
├── postmark_test.go
├── sendgrid_test.go
├── smtp_test.go
└── sparkpost_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
indent_size = 4
indent_style = tab
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
[*.{md,yml}]
indent_size = 2
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
Hello, we are very happy you decided to contribute to Go Mail. But before you start with your contribution,
please make sure to read through our guidelines:
- [Issue Reporting Guidline](#issue-reporting-guidline)
- [Pull Request Guidlines](#pull-request-guidlines)
## Issue Reporting Guideline
If you find a bug or believe that some important feature is missing you can open a new issue on the Github-Project page,
using our provided issue templates. Before creating a new issue, please make sure that there isn't already an issue
covering this problem or requesting this feature.
## Pull Request Guidelines
- The `main` branch always contains the latest stable released version and doesn't take PRs.
Instead, create dedicated feature branches and submit your PR to our `dev` branch.
- It's okay if your PR contains several small commits as we will squash the PR before merging it.
- Please try to use meaningful commit messages.
- Before creating a new PR, check if your code is linted correctly.
- If you want to add a new feature:
- Add a small but complete description of the new feature.
- Please provide a convincing reason why you think this feature needs to be added.
- If you add a bug fix:
- Please refer the corresponding issue, if one exists, in your PR.
- If no issue exist for the bug you fix you need to provide a detailed description of the error and if possible a live demo. Or create a new issue on our [Github page](https://github.com/ainsleyclark/go-mail/issues)
- Create unit tests for new features and use the `make all` command to test, lint and format.
================================================
FILE: .github/FUNDING.yml
================================================
github: [ainsleyclark]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: Create a report to help us improve Go Mail
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version**
Please specify a version number you are using.
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for Go Mail
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
# Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
================================================
FILE: .github/SECURITY.md
================================================
# Security Policy
## Reporting an Issue
If you need to report a security issue please email the author [here](mailto:info@ainsleyclark.com)
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '25 19 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
================================================
FILE: .github/workflows/email.yml
================================================
name: Email
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '30 1 1,15 * *'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Test
env:
# Email
EMAIL_TO: ${{ secrets.EMAIL_TO }}
EMAIL_CC: ${{ secrets.EMAIL_CC }}
EMAIL_BCC: ${{ secrets.EMAIL_BCC }}
# MailGun
MAILGUN_URL: ${{ secrets.MAILGUN_URL }}
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
MAILGUN_FROM_ADDRESS: ${{ secrets.MAILGUN_FROM_ADDRESS }}
MAILGUN_FROM_NAME: ${{ secrets.MAILGUN_FROM_NAME }}
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
# Postal
POSTAL_URL: ${{ secrets.POSTAL_URL }}
POSTAL_API_KEY: ${{ secrets.POSTAL_API_KEY }}
POSTAL_FROM_ADDRESS: ${{ secrets.POSTAL_FROM_ADDRESS }}
POSTAL_FROM_NAME: ${{ secrets.POSTAL_FROM_NAME }}
# Postmark
POSTMARK_API_KEY: ${{ secrets.POSTMARK_API_KEY }}
POSTMARK_FROM_ADDRESS: ${{ secrets.POSTMARK_FROM_ADDRESS }}
POSTMARK_FROM_NAME: ${{ secrets.POSTMARK_FROM_NAME }}
# SendGrid
SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
SENDGRID_FROM_ADDRESS: ${{ secrets.SENDGRID_FROM_ADDRESS }}
SENDGRID_FROM_NAME: ${{ secrets.SENDGRID_FROM_NAME }}
# SMTP
SMTP_URL: ${{ secrets.SMTP_URL }}
SMTP_FROM_ADDRESS: ${{ secrets.SMTP_FROM_ADDRESS }}
SMTP_FROM_NAME: ${{ secrets.SMTP_FROM_NAME }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
# SparkPost
SPARKPOST_URL: ${{ secrets.SPARKPOST_URL }}
SPARKPOST_API_KEY: ${{ secrets.SPARKPOST_API_KEY }}
SPARKPOST_FROM_ADDRESS: ${{ secrets.SPARKPOST_FROM_ADDRESS }}
SPARKPOST_FROM_NAME: ${{ secrets.SPARKPOST_FROM_NAME }}
run: |
# Make file runnable, might not be necessary
chmod +x ./bin/tests.sh
# Run tests
# Ignore Postal, no server active.
./bin/tests.sh mailgun
./bin/tests.sh postmark
./bin/tests.sh sendgrid
./bin/tests.sh smtp
./bin/tests.sh sparkpost
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Format
run: make format
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
skip-go-installation: true
skip-pkg-cache: true
args: --verbose
- name: Test
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2.1.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.out
- name: Diff
run: git diff
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Editors
.idea
.idea/*
# Sensitive Info
credentials.md
# System Files
.DS_Store
# Go Mail Specifc
.env
================================================
FILE: .golangci.yml
================================================
linters:
enable:
- gofmt
- govet
- gocyclo
- ineffassign
- thelper
- tparallel
- unconvert
- unparam
- wastedassign
- revive
run:
go: '1.17'
skip-dirs:
- res
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Ainsley Clark
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 the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Setup
setup:
sudo chmod +x ./bin/tests.sh
go mod tidy
.PHONY: setup
# Run gofmt
format:
go fmt ./...
.PHONY: format
# Run linter
lint:
golangci-lint run ./...
.PHONY: lint
# Test uses race and coverage
test:
go clean -testcache && go test -race $$(go list ./... | grep -v tests | grep -v examples | grep -v res | grep -v mocks) -coverprofile=coverage.out -covermode=atomic
.PHONY: test
# Test with -v
test-v:
go clean -testcache && go test -race -v $$(go list ./... | grep -v tests | grep -v examples | grep -v res | grep -v mocks) -coverprofile=coverage.out -covermode=atomic
.PHONY: test-v
# Runs real world tests for a driver or all drivers.
# See ./bin/tests.sh for example usage.
test-driver:
go clean -testcache && ./bin/tests.sh $(driver)
.PHONY: test-driver
# Run all the tests and opens the coverage report
cover: test
go tool cover -html=coverage.out
.PHONY: cover
# Make mocks keeping directory tree
mocks:
rm -rf internal/mocks && mockery --all --keeptree --output ./internal/mocks && mv ./internal/mocks/internal/* ./internal/mocks
.PHONY: mocks
# Run go doc
doc:
godoc -http localhost:8080
.PHONY: doc
# Make format, lint and test
all:
$(MAKE) format
$(MAKE) lint
$(MAKE) test
================================================
FILE: README.md
================================================
<div align="center">
<img height="300" src="res/logos/go-mail.svg?size=new2" alt="Go Mail Logo" />
[](http://golang.org)
[](https://goreportcard.com/report/github.com/ainsleyclark/go-mail)
[](https://github.com/ainsleyclark/go-mail/actions/workflows/test.yml)
[](https://codecov.io/gh/ainsleyclark/go-mail)
[](https://pkg.go.dev/github.com/ainsleyclark/go-mail)
[](https://twitter.com/ainsleydev)
</div>
# 📧 Go Mail
A cross-platform mail driver for GoLang. Featuring Mailgun, Postal, Postmark, SendGrid, SparkPost & SMTP.
## Overview
- ✅ Multiple mail drivers for your needs or even create your own custom Mailer.
- ✅ Direct dependency free, all requests are made with the standard lib http.Client.
- ✅ Send attachments with two struct fields, it's extremely simple.
- ✅ Send CC & BCC messages.
- ✅ Extremely lightweight.
## Supported API's
- <img align="left" src="res/logos/mailgun.svg" width="24" /> [Mailgun](https://documentation.mailgun.com/)
- <img align="left" src="res/logos/postal.svg" width="24" /> [Postal](https://docs.postalserver.io/)
- <img align="left" src="res/logos/postmark.png" width="24" /> [Postmark](https://postmarkapp.com/)
- <img align="left" src="res/logos/sendgrid.svg" width="24" /> [SendGrid](https://sendgrid.com/)
- <img align="left" src="res/logos/sparkpost.png?new=new" width="24" /> [SparkPost](https://www.sparkpost.com/)
- <img align="left" src="res/logos/smtp.svg" width="24" /> SMTP
## Introduction
Go Mail aims to unify multiple popular mail APIs into a singular, easy to use interface. Email sending is seriously
simple and great for allowing the developer or end user to choose what platform they use.
```go
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSparkPost(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
## Installation
```bash
go get -u github.com/ainsleyclark/go-mail
```
## Docs
Documentation can be found at the [Go Docs](https://pkg.go.dev/github.com/ainsleyclark/go-mail), but we have included a
kick-start guide below to get you started.
### Creating a new client:
You can create a new driver by calling the `drivers` package and passing in a configuration type which is required to
create a new mailer. Each platform requires its own data, for example, Mailgun requires a domain, but SparkPost doesn't.
This is based of the requirements for the API. For more details see the [examples](#Examples) below.
```go
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Client: http.DefaultClient, // Client is optional
}
mailer, err := drivers.NewSparkpost(cfg)
if err != nil {
log.Fatalln(err)
}
```
### Sending Data:
A transmission is required to transmit to a mailer as shown below. Once send is called, a `mail.Response` and an `error`
be returned indicating if the transmission was successful.
```go
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
Headers: map[string]string{
"X-Go-Mail": "Test",
},
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
### Response:
The mail response is used for debugging and inspecting results of the mailer. Below is the `Response` type.
```go
// Response represents the data passed back from a successful transmission.
type Response struct {
StatusCode int // e.g. 200
Body []byte // e.g. {"result: success"}
Headers http.Header // e.g. map[X-Ratelimit-Limit:[600]]
ID string // e.g "100"
Message string // e.g "Email sent successfully"
}
```
### Adding attachments:
Adding attachments to the transmission is as simple as passing a byte slice and filename. Go Mail takes care of the rest
for you.
```go
image, err := ioutil.ReadFile("gopher.jpg")
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
Attachments: []mail.Attachment{
{
Filename: "gopher.jpg",
Bytes: image,
},
},
}
```
## Examples
#### Mailgun
```go
cfg := mail.Config{
URL: "https://api.eu.mailgun.net", // Or https://api.mailgun.net
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Domain: "my-domain.com",
}
mailer, err := drivers.NewMailgun(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
#### Postal
```go
cfg := mail.Config{
URL: "https://postal.example.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewPostal(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
#### Postmark
```go
cfg := mail.Config{
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewPostmark(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
#### SendGrid
```go
cfg := mail.Config{
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSendGrid(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
#### SMTP
```go
cfg := mail.Config{
URL: "smtp.gmail.com",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Password: "my-password",
Port: 587,
}
mailer, err := drivers.NewSMTP(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
#### SparkPost
```go
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com", // Or https://api.sparkpost.com/api/v1
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSparkPost(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
CC: []string{"cc@gophers.com"},
BCC: []string{"bcc@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
```
## Writing a Mailable
You have the ability to create your own custom Mailer by implementing the singular method interface shown below.
```go
type Mailer interface {
// Send accepts a mail.Transmission to send an email through a particular
// driver/provider. Transmissions will be validated before sending.
//
// A mail.Response or an error will be returned. In some circumstances
// the body and status code will be attached to the response for debugging.
Send(t *mail.Transmission) (mail.Response, error)
}
```
## Debugging
To debug any errors or issues you are facing with Go Mail, you are able to change the `Debug` variable in the
`mail` package. This will write the HTTP requests in curl to stdout. Additional information will also be
displayed in the errors such as method operations.
```go
mail.Debug = true
```
## Development
### Setup
To get set up with Go Mail simply clone the repo and run the following:
```bash
go get github.com/vektra/mockery/v2/.../
make setup
make mocks
```
## Env
All secrets are contained within the `.env` file for testing drivers. To begin with, make a copy of the `.env.example`
file and name it `.env`. You can the set the environment variables to match your credentials for the mail drivers.
You can set the recipients of emails by modifying the `EMAIL` variables as show below.
- `EMAIL_TO`: Recipients of test emails in a comma delimited list.
- `EMAIL_CC`: CC recipients of test emails in a comma delimited list.
- `EMAIL_BCC`: BCC recipients of test emails in a comma delimited list.
### Testing
To run all driver tests, execute the following command:
```bash
make test-driver
```
To run a specific driver test, prepend the `driver` flag as show below:
```bash
make test-driver driver=sparkpost
```
The driver flag can be one of the following:
- `mailgun`
- `postal`
- `postmark`
- `sendgrid`
- `smtp`
- `sparkpost`
## Contributing
We welcome contributors, but please read the [contributing document](CONTRIBUTING.md) before making a pull request.
## Credits
Shout out to the incredible [Maria Letta](https://github.com/MariaLetta) for her excellent Gopher illustrations.
## Licence
Code Copyright 2022 Go Mail. Code released under the [MIT Licence](LICENCE).
================================================
FILE: bin/tag.sh
================================================
#!/bin/bash
#
# tag.sh
#
# Set variables
version=$1
message=$2
# Check version is not empty
if [[ $version == "" ]]
then
echo "Add Version number"
exit
fi
# Check commit message is not empty
if [[ $message == "" ]]
then
echo "Add commit message"
exit
fi
echo "Releasing version: " $version
git tag -a "$version" -m "$message"
git push origin $version
================================================
FILE: bin/tests.sh
================================================
#!/usr/bin/bash
# Shell script for executing tests based on input.
# Usage:
# ./tests.sh for all drivers
# ./tests.sh sparkpost for a particular driver
# Author - Ainsley Clark
DRIVER=$1
declare -A tests=(
["mailgun"]="Test_MailGun"
["postal"]="Test_Postal"
["postmark"]="Test_Postmark"
["sendgrid"]="Test_SendGrid"
["smtp"]="Test_SMTP"
["sparkpost"]="Test_SparkPost"
)
if [ -z "$DRIVER" ]
then
for name in "${!tests[@]}";
do go test -v ./tests/ -run "${tests[$name]}";
done
else
go test -v ./tests/ -run "${tests["$DRIVER"]}";
fi
================================================
FILE: drivers/drivers.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import "github.com/ainsleyclark/go-mail/internal/httputil"
var (
// newJSONData is an alias for httputil.NewJSONData
// for creating JSON payloads.
newJSONData = httputil.NewJSONData
// formDataFn is an alias for httputil.NewFormData
// for creating form data payloads.
newFormData = httputil.NewFormData
)
================================================
FILE: drivers/drivers_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"errors"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"net/http"
"os"
"path/filepath"
"testing"
)
// DriversTestSuite defines the helper used for mail
// testing.
type DriversTestSuite struct {
suite.Suite
base string
}
// Assert testing has begun.
func TestMail(t *testing.T) {
suite.Run(t, new(DriversTestSuite))
}
// Assigns test base.
func (t *DriversTestSuite) SetupSuite() {
wd, err := os.Getwd()
t.NoError(err)
t.base = wd
}
const (
// DataPath defines where the test data resides.
DataPath = "testdata"
)
var (
// Trans is the transmission used for testing.
Trans = &mail.Transmission{
Recipients: []string{"recipient@test.com"},
CC: []string{"cc@test.com"},
BCC: []string{"bcc@test.com"},
Subject: "Subject",
HTML: "<h1>HTML</h1>",
PlainText: "PlainText",
Headers: map[string]string{
"X-Go-Mail": "Test",
},
}
// Trans is the transmission with an
// attachment used for testing.
TransWithAttachment = &mail.Transmission{
Recipients: []string{"recipient@test.com"},
Subject: "Subject",
HTML: "<h1>HTML</h1>",
PlainText: "PlainText",
Attachments: []mail.Attachment{{Filename: "test.jpg"}},
}
// Config is the default configuration used
// for testing.
Comfig = mail.Config{
URL: "my-url",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Domain: "my-domain",
}
)
// Returns a PNG attachment for testing.
func (t *DriversTestSuite) Attachment(name string) mail.Attachment {
path := filepath.Join(t.base, DataPath, name)
file, err := os.ReadFile(path)
if err != nil {
t.Fail("error getting attachment with the path: "+path, err)
}
return mail.Attachment{
Filename: name,
Bytes: file,
}
}
func (t *DriversTestSuite) UtilTestUnmarshal(r httputil.Responder, buf []byte) {
errBuf := []byte("wrong")
err := r.Unmarshal(errBuf)
t.Error(err)
err = r.Unmarshal(buf)
t.NoError(err)
}
func (t *DriversTestSuite) UtilTestMeta(r httputil.Responder, message, id string) {
got := r.Meta()
t.Equal(message, got.Message)
t.Equal(id, got.ID)
}
func (t *DriversTestSuite) UtilTestSend(fn func(m *mocks.Requester) mail.Mailer, json bool) {
res := mail.Response{
StatusCode: http.StatusOK,
Body: []byte("body"),
Headers: nil,
ID: "1",
Message: "success",
}
tt := map[string]struct {
input *mail.Transmission
mock func(m *mocks.Requester)
jsonFn func(obj interface{}) (*httputil.JSONData, error)
want interface{}
}{
"Success": {
Trans,
func(m *mocks.Requester) {
m.On("Do", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(res, nil)
},
httputil.NewJSONData,
res,
},
"With Attachment": {
TransWithAttachment,
func(m *mocks.Requester) {
m.On("Do", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(res, nil)
},
httputil.NewJSONData,
res,
},
"Validation Failed": {
nil,
nil,
httputil.NewJSONData,
"can't validate a nil transmission",
},
"JSON Error": {
Trans,
func(m *mocks.Requester) {
m.On("Do", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(mail.Response{}, errors.New("send error"))
},
func(obj interface{}) (*httputil.JSONData, error) {
return nil, errors.New("json error")
},
"json error",
},
"Send Error": {
Trans,
func(m *mocks.Requester) {
m.On("Do", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(mail.Response{}, errors.New("send error"))
},
httputil.NewJSONData,
"send error",
},
}
for name, test := range tt {
if name == "JSON Error" && !json {
continue
}
t.Run(name, func() {
orig := newJSONData
defer func() { newJSONData = orig }()
newJSONData = test.jsonFn
requester := &mocks.Requester{}
if test.mock != nil {
test.mock(requester)
}
m := fn(requester)
got, err := m.Send(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.Equal(test.want, got)
})
}
}
================================================
FILE: drivers/mailgun.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/ainsleyclark/go-mail/internal/client"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"net/http"
"strings"
)
// mailgun represents the entity for sending mail via the
// Mailgun API.
//
// See:
// https://documentation.mailgun.com/en/latest/api_reference.html
// https://documentation.mailgun.com/en/latest/api-sending.html
type mailGun struct {
cfg mail.Config
client client.Requester
}
const (
// mailgunEndpoint defines the endpoint to POST to.
mailgunEndpoint = "/v3/%s/messages"
)
// NewMailgun creates a new Mailgun client. Configuration
// is validated before initialisation.
func NewMailgun(cfg mail.Config) (mail.Mailer, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
if cfg.Domain == "" {
return nil, errors.New("driver requires a domain")
}
return &mailGun{
cfg: cfg,
client: client.New(cfg.Client),
}, nil
}
type (
// mailGunResponse defines the data sent back from the MailGun API.
// ID is included on successful transmission.
//
// Example JSON Responses:
// {"id":"<20211229082318.a988bed7abe472bd@sandboxa6807a568a404524b2b216817d7ed775.mailgun.org>","message":"Queued. Thank you."}
// {"message":"Need at least one of 'text' or 'html' parameters specified"}
// {"message":"from parameter is missing"}
mailgunResponse struct {
Message string `json:"message"`
ID string `json:"id,omitempty"`
}
)
func (r *mailgunResponse) Unmarshal(buf []byte) error {
resp := &mailgunResponse{}
err := json.Unmarshal(buf, resp)
if err != nil {
return err
}
*r = *resp
return nil
}
func (r *mailgunResponse) CheckError(response *http.Response, buf []byte) error {
if client.Is2XX(response.StatusCode) {
return nil
}
if len(buf) == 0 {
return mail.ErrEmptyBody
}
return errors.New(r.Message)
}
func (r *mailgunResponse) Meta() httputil.Meta {
return httputil.Meta{
Message: r.Message,
ID: r.ID,
}
}
func (m *mailGun) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
f := newFormData()
f.AddValue("from", fmt.Sprintf("%s <%s>", m.cfg.FromName, m.cfg.FromAddress))
f.AddValue("subject", t.Subject)
f.AddValue("html", t.HTML)
f.AddValue("text", t.PlainText)
for _, to := range t.Recipients {
f.AddValue("to", to)
}
if t.HasCC() {
for _, c := range t.CC {
f.AddValue("cc", c)
}
}
if t.HasBCC() {
for _, b := range t.BCC {
f.AddValue("bcc", b)
}
}
if t.HasAttachments() {
for _, v := range t.Attachments {
f.AddBuffer("attachment", v.Filename, v.Bytes)
}
}
for k, v := range t.Headers {
f.AddValue("h:"+k, v)
}
url := fmt.Sprintf("%s/%s", m.cfg.URL, strings.TrimPrefix(fmt.Sprintf(mailgunEndpoint, m.cfg.Domain), "/"))
req := httputil.NewHTTPRequest(http.MethodPost, url)
req.SetBasicAuth("api", m.cfg.APIKey)
return m.client.Do(context.Background(), req, f, &mailgunResponse{})
}
================================================
FILE: drivers/mailgun_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"errors"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"log"
"net/http"
)
func ExampleNewMailgun() {
cfg := mail.Config{
URL: "https://api.eu.mailgun.net", // Or https://api.mailgun.net
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Domain: "my-domain.com",
}
_, err := NewMailgun(cfg)
if err != nil {
log.Fatalln(err)
}
}
func (t *DriversTestSuite) TestNewMailGun() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
URL: "https://mailgun.example.com",
FromAddress: "addr",
FromName: "name",
APIKey: "key",
Domain: "domain",
},
nil,
},
"Validation Failed": {
mail.Config{},
"driver requires from address",
},
"No Domain": {
mail.Config{
FromName: "name",
FromAddress: "hello@gophers.com",
APIKey: "key",
},
"driver requires a domain",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewMailgun(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestMailgunResponse_Unmarshal() {
t.UtilTestUnmarshal(&mailgunResponse{}, []byte(`{"message": "Hello"}`))
}
func (t *DriversTestSuite) TestMailgunResponse_CheckError() {
tt := map[string]struct {
response *http.Response
buf []byte
want error
}{
"2xx": {
&http.Response{StatusCode: http.StatusOK},
[]byte("test"),
nil,
},
"Empty Body": {
&http.Response{StatusCode: http.StatusInternalServerError},
nil,
mail.ErrEmptyBody,
},
"Error": {
&http.Response{StatusCode: http.StatusInternalServerError},
[]byte("test"),
errors.New("error"),
},
}
for name, test := range tt {
t.Run(name, func() {
resp := mailgunResponse{Message: "error"}
err := resp.CheckError(test.response, test.buf)
if err != nil {
t.Contains(err.Error(), test.want.Error())
return
}
t.Equal(test.want, err)
})
}
}
func (t *DriversTestSuite) TestMailgunResponse_Meta() {
d := &mailgunResponse{Message: "Success", ID: "id"}
t.UtilTestMeta(d, d.Message, d.ID)
}
func (t *DriversTestSuite) TestMailGun_Send() {
t.UtilTestSend(func(m *mocks.Requester) mail.Mailer {
return &mailGun{cfg: Comfig, client: m}
}, false)
}
================================================
FILE: drivers/postal.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/ainsleyclark/go-mail/internal/client"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"net/http"
)
// postal represents the entity for sending mail via the
// Postal API.
//
// See:
// https://docs.postalserver.io/developer/api
// https://apiv1.postalserver.io/controllers/send/message.html
type postal struct {
cfg mail.Config
client client.Requester
}
const (
// postalEndpoint defines the endpoint to POST to.
postalEndpoint = "%s/api/v1/send/message"
// postalErrorMessage defines the message when an error occurred
// when sending mail via the Postal API.
postalErrorMessage = "error sending transmission to Postal API"
)
// NewPostal creates a new Postal client. Configuration
// is validated before initialisation.
func NewPostal(cfg mail.Config) (mail.Mailer, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
return &postal{
cfg: cfg,
client: client.New(cfg.Client),
}, nil
}
type (
// postalTransmission defines the data to be sent to the Postal API.
postalTransmission struct {
To []string `json:"to"`
CC []string `json:"cc"`
BCC []string `json:"bcc"`
From string `json:"from"`
Sender string `json:"sender"`
Subject string `json:"subject"`
HTML string `json:"html_body"`
PlainText string `json:"plain_body"`
Attachments []postalAttachment `json:"attachments"`
Headers map[string]string `json:"headers"`
}
// postalAttachment defines a singular Postal mail attachment.
postalAttachment struct {
Name string `json:"name"`
ContentType string `json:"content_type"`
Data string `json:"data"`
}
// postalResponse defines the data sent back from the Postal API.
// Status can either be "success" or "error" and data is
// dynamic dependent on if an error occurred during processing.
//
// Example JSON Responses:
// {"status":"success","time":0.08,"flags":{},"data":{"message_id":"080c21de-52f9-4be1-9cbe-19d63450949c@rp.postal.example.com","messages":{"info@ainsleyclark.com":{"id":28,"token":"WEjrFfpnynRm"}}}}
// {"status":"error","time":0.0,"flags":{},"data":{"code":"NoRecipients","message":"There are no recipients defined to receive this message"}}
postalResponse struct {
Status string `json:"status"`
Time float32 `json:"time"`
Flags map[string]interface{} `json:"flags"`
Data map[string]interface{} `json:"data"`
}
)
func (r *postalResponse) Unmarshal(buf []byte) error {
resp := &postalResponse{}
err := json.Unmarshal(buf, resp)
if err != nil {
return err
}
*r = *resp
return nil
}
func (r *postalResponse) CheckError(response *http.Response, buf []byte) error {
if r.Status == "success" {
return nil
}
if len(buf) == 0 {
return mail.ErrEmptyBody
}
msg := postalErrorMessage
if code, ok := r.Data["code"]; ok {
msg = fmt.Sprintf("%s - code: %s", msg, code)
}
if message, ok := r.Data["message"]; ok {
msg = fmt.Sprintf("%s, message: %s", msg, message)
}
return errors.New(msg)
}
func (r *postalResponse) Meta() httputil.Meta {
m := httputil.Meta{
Message: "Successfully sent Postal email",
}
if val, ok := r.Data["message_id"]; ok {
m.ID = fmt.Sprintf("%v", val)
}
return m
}
func (d *postal) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
tx := postalTransmission{
To: t.Recipients,
CC: t.CC,
BCC: t.BCC,
From: d.cfg.FromAddress,
Sender: d.cfg.FromName,
Subject: t.Subject,
HTML: t.HTML,
PlainText: t.PlainText,
}
if t.HasAttachments() {
for _, v := range t.Attachments {
tx.Attachments = append(tx.Attachments, postalAttachment{
Name: v.Filename,
ContentType: v.Mime(),
Data: v.B64(),
})
}
}
tx.Headers = t.Headers
pl, err := newJSONData(tx)
if err != nil {
return mail.Response{}, err
}
req := httputil.NewHTTPRequest(http.MethodPost, fmt.Sprintf(postalEndpoint, d.cfg.URL))
req.AddHeader("X-Server-API-Key", d.cfg.APIKey)
return d.client.Do(context.Background(), req, pl, &postalResponse{})
}
================================================
FILE: drivers/postal_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"fmt"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"log"
"net/http"
)
func ExampleNewPostal() {
cfg := mail.Config{
URL: "https://postal.example.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
_, err := NewPostal(cfg)
if err != nil {
log.Fatalln(err)
}
}
func (t *DriversTestSuite) TestNewPostal() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
URL: "https://postal.example.com",
APIKey: "key",
FromAddress: "addr",
FromName: "name",
},
nil,
},
"Validation Failed": {
mail.Config{},
"driver requires from address",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewPostal(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestPostalResponse_Unmarshal() {
t.UtilTestUnmarshal(&postalResponse{}, []byte(`{"status": "success"}`))
}
func (t *DriversTestSuite) TestPostalResponse_CheckError() {
tt := map[string]struct {
input postalResponse
response *http.Response
buf []byte
want error
}{
"Success": {
postalResponse{Status: "success"},
&http.Response{StatusCode: http.StatusOK},
[]byte("test"),
nil,
},
"Empty Body": {
postalResponse{},
&http.Response{StatusCode: http.StatusInternalServerError},
nil,
mail.ErrEmptyBody,
},
"Error": {
postalResponse{Data: map[string]interface{}{"code": "code", "message": "message"}},
&http.Response{StatusCode: http.StatusInternalServerError},
[]byte("test"),
fmt.Errorf("%s - code: code, message: message", postalErrorMessage),
},
}
for name, test := range tt {
t.Run(name, func() {
err := test.input.CheckError(test.response, test.buf)
if err != nil {
t.Contains(err.Error(), test.want.Error())
return
}
t.Equal(test.want, err)
})
}
}
func (t *DriversTestSuite) TestPostalResponse_Meta() {
d := &postalResponse{
Data: map[string]interface{}{"message_id": 10},
}
t.UtilTestMeta(d, "Successfully sent Postal email", "10")
}
func (t *DriversTestSuite) TestPostal_Send() {
t.UtilTestSend(func(m *mocks.Requester) mail.Mailer {
return &postal{cfg: Comfig, client: m}
}, true)
}
================================================
FILE: drivers/postmark.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"context"
"encoding/json"
"fmt"
"github.com/ainsleyclark/go-mail/internal/client"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"net/http"
"strings"
"time"
)
// postal represents the entity for sending mail via the
// Postmark API.
//
// See: https://postmarkapp.com/developer/api/email-api
type postmark struct {
cfg mail.Config
client client.Requester
}
const (
// postalEndpoint defines the endpoint to POST to.
postmarkEndpoint = "https://api.postmarkapp.com/email"
// postmarkErrorMessage defines the message when an error occurred
// when sending mail via the Postmark API.
postmarkErrorMessage = "error sending transmission to Postmark API"
)
// NewPostmark creates a new Postmark client. Configuration
// is validated before initialisation.
func NewPostmark(cfg mail.Config) (mail.Mailer, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
return &postmark{
cfg: cfg,
client: client.New(cfg.Client),
}, nil
}
type (
// postmarkTransmission defines the data to be sent to the Postmark API.
postmarkTransmission struct {
From string `json:"From"`
To string `json:"To"`
CC string `json:"Cc"`
BCC string `json:"Bcc"`
Subject string `json:"Subject"`
Tag string `json:"Tag"`
HTML string `json:"HtmlBody"`
PlainText string `json:"TextBody"`
ReplyTo string `json:"ReplyTo"`
Headers []postmarkHeader `json:"headers"`
TrackOpens bool `json:"TrackOpens"`
TrackLinks string `json:"TrackLinks"`
Attachments []postmarkAttachment `json:"Attachments"`
Metadata struct {
Color string `json:"color"`
ClientID string `json:"client-id"`
} `json:"Metadata"`
MessageStream string `json:"MessageStream"`
}
// postmarkHeaders defines the key value pair of custom headers
// to send with the email.
postmarkHeader struct {
Name string `json:"Name"`
Value string `json:"Value"`
}
// postmarkAttachment defines a singular Postmark mail attachment.
postmarkAttachment struct {
Name string `json:"Name"`
Content string `json:"Content"`
ContentType string `json:"ContentType"`
ContentID string `json:"ContentID,omitempty"`
}
// postmarkResponse defines the data sent back from the Postmark API.
// An error code of 0 represents a successful transmission.
//
// Example JSON Responses:
// {"To":"info@ainsleyclark.com","SubmittedAt":"2021-12-29T15:58:17.8637679Z","MessageID":"947125ed-9e43-4dce-b66c-def49198b3d3","ErrorCode":0,"Message":"OK"}
// {"ErrorCode":300,"Message":"Zero recipients specified"}
postmarkResponse struct {
To string `json:"To"`
SubmittedAt time.Time `json:"SubmittedAt"`
ID string `json:"MessageID"`
ErrorCode int `json:"ErrorCode"`
Message string `json:"Message"`
}
)
func (r *postmarkResponse) Unmarshal(buf []byte) error {
resp := &postmarkResponse{}
err := json.Unmarshal(buf, resp)
if err != nil {
return err
}
*r = *resp
return nil
}
func (r *postmarkResponse) CheckError(response *http.Response, buf []byte) error {
if r.ErrorCode == 0 {
return nil
}
if len(buf) == 0 {
return mail.ErrEmptyBody
}
return fmt.Errorf("%s - code: %d, message: %s", postmarkErrorMessage, r.ErrorCode, r.Message)
}
func (r *postmarkResponse) Meta() httputil.Meta {
return httputil.Meta{
Message: r.Message,
ID: r.ID,
}
}
func (d *postmark) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
tx := postmarkTransmission{
To: strings.Join(t.Recipients, ","),
CC: strings.Join(t.CC, ","),
BCC: strings.Join(t.BCC, ","),
From: fmt.Sprintf("%s <%s>", d.cfg.FromName, d.cfg.FromAddress),
Subject: t.Subject,
HTML: t.HTML,
PlainText: t.PlainText,
MessageStream: "outbound",
}
if t.HasAttachments() {
for _, v := range t.Attachments {
tx.Attachments = append(tx.Attachments, postmarkAttachment{
Name: v.Filename,
ContentType: v.Mime(),
Content: v.B64(),
})
}
}
for k, v := range t.Headers {
tx.Headers = append(tx.Headers, postmarkHeader{
Name: k,
Value: v,
})
}
pl, err := newJSONData(tx)
if err != nil {
return mail.Response{}, err
}
req := httputil.NewHTTPRequest(http.MethodPost, postmarkEndpoint)
req.AddHeader("X-Postmark-Server-Token", d.cfg.APIKey)
return d.client.Do(context.Background(), req, pl, &postmarkResponse{})
}
================================================
FILE: drivers/postmark_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"fmt"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"log"
"net/http"
)
func ExampleNewPostmark() {
cfg := mail.Config{
URL: "https://postal.example.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
_, err := NewPostal(cfg)
if err != nil {
log.Fatalln(err)
}
}
func (t *DriversTestSuite) TestNewPostmark() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
APIKey: "key",
FromAddress: "addr",
FromName: "name",
},
nil,
},
"Validation Failed": {
mail.Config{},
"driver requires from address",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewPostmark(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestPostmarkResponse_Unmarshal() {
t.UtilTestUnmarshal(&postmarkResponse{}, []byte(`{"message": "Hello"}`))
}
func (t *DriversTestSuite) TestPostmarkResponse_CheckError() {
tt := map[string]struct {
input postmarkResponse
response *http.Response
buf []byte
want error
}{
"Success": {
postmarkResponse{ErrorCode: 0},
&http.Response{StatusCode: http.StatusOK},
[]byte("test"),
nil,
},
"Empty Body": {
postmarkResponse{ErrorCode: 10},
&http.Response{StatusCode: http.StatusInternalServerError},
nil,
mail.ErrEmptyBody,
},
"Error": {
postmarkResponse{ErrorCode: 10, Message: "message"},
&http.Response{StatusCode: http.StatusInternalServerError},
[]byte("test"),
fmt.Errorf("%s - code: 10, message: message", postmarkErrorMessage),
},
}
for name, test := range tt {
t.Run(name, func() {
err := test.input.CheckError(test.response, test.buf)
if err != nil {
t.Contains(err.Error(), test.want.Error())
return
}
t.Equal(test.want, err)
})
}
}
func (t *DriversTestSuite) TestPostmarkResponse_Meta() {
d := &postmarkResponse{Message: "Success", ID: "id"}
t.UtilTestMeta(d, d.Message, d.ID)
}
func (t *DriversTestSuite) TestPostmark_Send() {
t.UtilTestSend(func(m *mocks.Requester) mail.Mailer {
return &postmark{cfg: Comfig, client: m}
}, true)
}
================================================
FILE: drivers/sendgrid.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"context"
"encoding/json"
"fmt"
"github.com/ainsleyclark/go-mail/internal/client"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"net/http"
)
// sendGrid represents the entity for sending mail via the
// SendGrid API.
//
// See:
// https://docs.sendgrid.com/api-reference/how-to-use-the-sendgrid-v3-api
// https://docs.sendgrid.com/api-reference/mail-send/mail-send
type sendGrid struct {
cfg mail.Config
client client.Requester
}
const (
// sendGridEndpoint defines the endpoint to POST to.
// The host for Web API v3 requests is always https://sendgrid.com/v3/
sendGridEndpoint = "https://api.sendgrid.com/v3/mail/send"
// sendgridErrorMessage defines the message when an error occurred
// when sending mail via the SendGrid API.
sendgridErrorMessage = "error sending transmission to SendGrid API"
)
// NewSendGrid creates a new sendGrid client. Configuration
// is validated before initialisation.
func NewSendGrid(cfg mail.Config) (mail.Mailer, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
return &sendGrid{
cfg: cfg,
client: client.New(cfg.Client),
}, nil
}
type (
// postalTransmission defines the data to be sent to the sendGrid API.
sgTransmission struct {
From *sgEmail `json:"from,omitempty"`
Subject string `json:"subject,omitempty"`
Personalizations []*sgPersonalization `json:"personalizations,omitempty"`
Content []*sgContent `json:"content,omitempty"`
Attachments []*sgAttachment `json:"attachments,omitempty"`
TemplateID string `json:"template_id,omitempty"`
Sections map[string]string `json:"sections,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Categories []string `json:"categories,omitempty"`
CustomArgs map[string]string `json:"custom_args,omitempty"`
SendAt int `json:"send_at,omitempty"`
BatchID string `json:"batch_id,omitempty"`
IPPoolID string `json:"ip_pool_name,omitempty"`
ReplyTo *sgEmail `json:"reply_to,omitempty"`
}
// sgPersonalization holds the mail body struct.
sgPersonalization struct {
To []*sgEmail `json:"to,omitempty"`
From *sgEmail `json:"from,omitempty"`
CC []*sgEmail `json:"cc,omitempty"`
BCC []*sgEmail `json:"bcc,omitempty"`
Subject string `json:"subject,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Substitutions map[string]string `json:"substitutions,omitempty"`
CustomArgs map[string]string `json:"custom_args,omitempty"`
DynamicTemplateData map[string]interface{} `json:"dynamic_template_data,omitempty"`
Categories []string `json:"categories,omitempty"`
SendAt int `json:"send_at,omitempty"`
}
// sgEmail holds email name and address info.
sgEmail struct {
Name string `json:"name,omitempty"`
Address string `json:"email,omitempty"`
}
// sgContent defines content of the mail body.
sgContent struct {
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
}
// sgAttachment holds attachment information.
sgAttachment struct {
Content string `json:"content,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Filename string `json:"filename,omitempty"`
Disposition string `json:"disposition,omitempty"`
ContentID string `json:"content_id,omitempty"`
}
// sgResponse contains the response data from the SendGrid
// API.
// Note: No response data is passed if the response code is 2xx
//
// Example JSON Response:
// {"errors":[{"message":"The from object must be provided for every email send. It is an object that requires the email parameter, but may also contain a name parameter. e.g. {\"email\" : \"example@example.com\"} or {\"email\" : \"example@example.com\", \"name\" : \"Example Recipient\"}.","field":"from.email","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.from"}]}
sgResponse struct {
Errors []sgError `json:"errors"`
}
// sgError defines a singular validation error from the API.
sgError struct {
Message string `json:"message"`
Field string `json:"field"`
Help string `json:"help"`
}
)
func (r *sgResponse) Unmarshal(buf []byte) error {
if len(buf) == 0 {
return nil
}
resp := &sgResponse{}
err := json.Unmarshal(buf, resp)
if err != nil {
return err
}
*r = *resp
return nil
}
func (r *sgResponse) CheckError(response *http.Response, buf []byte) error {
if client.Is2XX(response.StatusCode) {
return nil
}
if len(r.Errors) == 0 {
return nil
}
return fmt.Errorf("%s - message: %s, field: %s, help: %s", sendgridErrorMessage, r.Errors[0].Message, r.Errors[0].Field, r.Errors[0].Help)
}
func (r *sgResponse) Meta() httputil.Meta {
return httputil.Meta{
Message: "Successfully sent SendGrid email",
// No response data from SendGrid
ID: "",
}
}
func (d *sendGrid) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
tx := sgTransmission{
From: &sgEmail{
Name: d.cfg.FromName,
Address: d.cfg.FromAddress,
},
Subject: t.Subject,
Personalizations: []*sgPersonalization{
{Subject: t.Subject},
},
Content: []*sgContent{
{Type: "text/plain", Value: t.PlainText},
{Type: "text/html", Value: t.HTML},
},
Attachments: nil,
}
for _, r := range t.Recipients {
tx.Personalizations[0].To = append(tx.Personalizations[0].To, &sgEmail{
Address: r,
})
}
if t.HasCC() {
for _, c := range t.CC {
tx.Personalizations[0].CC = append(tx.Personalizations[0].CC, &sgEmail{
Address: c,
})
}
}
if t.HasBCC() {
for _, b := range t.BCC {
tx.Personalizations[0].BCC = append(tx.Personalizations[0].BCC, &sgEmail{
Address: b,
})
}
}
if t.HasAttachments() {
for _, v := range t.Attachments {
tx.Attachments = append(tx.Attachments, &sgAttachment{
Content: v.B64(),
Type: v.Mime(),
Name: "",
Filename: v.Filename,
Disposition: "attachment",
})
}
}
tx.Headers = t.Headers
pl, err := newJSONData(tx)
if err != nil {
return mail.Response{}, err
}
req := httputil.NewHTTPRequest(http.MethodPost, sendGridEndpoint)
req.AddHeader("Authorization", "Bearer "+d.cfg.APIKey)
return d.client.Do(context.Background(), req, pl, &sgResponse{})
}
================================================
FILE: drivers/sendgrid_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"fmt"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"log"
"net/http"
)
func ExampleNewSendGrid() {
cfg := mail.Config{
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
_, err := NewSendGrid(cfg)
if err != nil {
log.Fatalln(err)
}
}
func (t *DriversTestSuite) TestNewSendGrid() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
URL: "https://sendgrid.example.com",
APIKey: "key",
FromAddress: "addr",
FromName: "name",
},
nil,
},
"Validation Failed": {
mail.Config{},
"driver requires from address",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewSendGrid(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestSendGridResponse_Unmarshal() {
t.UtilTestUnmarshal(&sgResponse{}, []byte(`{"errors": []}`))
res := sgResponse{}
err := res.Unmarshal(nil)
t.NoError(err)
}
func (t *DriversTestSuite) TestSendGridResponse_CheckError() {
tt := map[string]struct {
input sgResponse
response *http.Response
buf []byte
want error
}{
"Success": {
sgResponse{Errors: nil},
&http.Response{StatusCode: http.StatusOK},
[]byte("test"),
nil,
},
"No Errors": {
sgResponse{},
&http.Response{StatusCode: http.StatusInternalServerError},
nil,
nil,
},
"Error": {
sgResponse{Errors: []sgError{{Message: "message", Field: "field", Help: "help"}}},
&http.Response{StatusCode: http.StatusInternalServerError},
[]byte("test"),
fmt.Errorf("%s - message: message, field: field, help: help", sendgridErrorMessage),
},
}
for name, test := range tt {
t.Run(name, func() {
err := test.input.CheckError(test.response, test.buf)
if err != nil {
t.Contains(err.Error(), test.want.Error())
return
}
t.Equal(test.want, err)
})
}
}
func (t *DriversTestSuite) TestSendGridResponse_Meta() {
d := &sgResponse{}
t.UtilTestMeta(d, "Successfully sent SendGrid email", "")
}
func (t *DriversTestSuite) TestSendGrid_Send() {
t.UtilTestSend(func(m *mocks.Requester) mail.Mailer {
return &sendGrid{cfg: Comfig, client: m}
}, true)
}
================================================
FILE: drivers/smtp.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"bytes"
"errors"
"fmt"
"github.com/ainsleyclark/go-mail/mail"
"mime/multipart"
"net/http"
"net/smtp"
"strconv"
"strings"
)
// smtpClient represents the data for sending mail via
// plain ol SMTP. Configuration, the client and the
// main send function are parsed for sending
// data.
type smtpClient struct {
cfg mail.Config
send smtpSendFunc
}
// smtpSendFunc defines the function for ending
// SMTP mail.Transmissions.
type smtpSendFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error
// NewSMTP creates a new smtp client. Configuration
// is validated before initialisation.
func NewSMTP(cfg mail.Config) (mail.Mailer, error) {
if cfg.URL == "" {
return nil, errors.New("driver requires a url")
}
if cfg.FromAddress == "" {
return nil, errors.New("driver requires from address")
}
if cfg.FromName == "" {
return nil, errors.New("driver requires from name")
}
if cfg.Password == "" {
return nil, errors.New("driver requires a password")
}
return &smtpClient{
cfg: cfg,
send: smtp.SendMail,
}, nil
}
// Send mail via plain SMTP. mail.Transmissions are validated
// before sending and attachments are added. Returns
// an error upon failure.
func (m *smtpClient) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
auth := smtp.PlainAuth("", m.cfg.FromAddress, m.cfg.Password, m.cfg.URL)
err = m.send(m.cfg.URL+":"+strconv.Itoa(m.cfg.Port), auth, m.cfg.FromAddress, m.getTo(t), m.bytes(t))
if err != nil {
return mail.Response{}, err
}
return mail.Response{
StatusCode: http.StatusOK,
Message: "Email sent successfully",
}, nil
}
// getTo returns the merged mail.Transmission recipients, CC and
// BCC email addresses.
func (m *smtpClient) getTo(t *mail.Transmission) []string {
var to []string
to = append(t.Recipients, t.CC...)
to = append(to, t.BCC...)
return to
}
// Processes the mail.Transmission and returns the bytes for
// sending. Mime types are set dependent on the
// content passed.
// See: https://gist.github.com/tylermakin/d820f65eb3c9dd98d58721c7fb1939a8?permalink_comment_id=2703291
func (m *smtpClient) bytes(t *mail.Transmission) []byte {
buf := bytes.NewBuffer(nil)
for k, v := range t.Headers {
buf.WriteString(fmt.Sprintf("%s: %s\n", k, v))
}
buf.WriteString("MIME-Version: 1.0\n")
writer := multipart.NewWriter(buf)
boundary := writer.Boundary()
if t.HasAttachments() {
buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%s\r\n", boundary))
} else {
buf.WriteString(fmt.Sprintf("Content-Type: text/html; charset=UTF-8; boundary=%s\r\n", boundary))
}
buf.WriteString(fmt.Sprintf("Subject: %s\n", t.Subject))
buf.WriteString(fmt.Sprintf("To: %s\n", strings.Join(t.Recipients, ",")))
if t.HasCC() {
buf.WriteString(fmt.Sprintf("CC: %s\n", strings.Join(t.CC, ",")))
}
buf.WriteString("\n")
if t.PlainText != "" {
buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
buf.WriteString(fmt.Sprintf("\r\n%s\r\n\n", strings.TrimSpace(t.PlainText)))
}
if t.HTML != "" {
buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
buf.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
buf.WriteString(fmt.Sprintf("\r\n%s\r\n\n", t.HTML))
}
if t.HasAttachments() {
for _, v := range t.Attachments {
buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
buf.WriteString(fmt.Sprintf("Content-Type: %s\n", v.Mime()))
buf.WriteString("Content-Transfer-Encoding: base64\n")
buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", v.Filename))
buf.WriteString(fmt.Sprintf("\r\n--%s", v.B64()))
}
buf.WriteString("--")
}
buf.WriteString(fmt.Sprintf("--%s--\n", boundary))
return buf.Bytes()
}
================================================
FILE: drivers/smtp_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"errors"
"fmt"
"github.com/ainsleyclark/go-mail/mail"
"net/smtp"
)
func (t *DriversTestSuite) TestNewSMTP() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
URL: "https://smtp.example.com",
FromAddress: "addr",
FromName: "name",
Password: "password",
},
nil,
},
"No url": {
mail.Config{},
"driver requires a url",
},
"No From Address": {
mail.Config{
URL: "https://smtp.example.com",
},
"driver requires from address",
},
"No From Name": {
mail.Config{
URL: "https://smtp.example.com",
FromAddress: "hello@gophers.com",
},
"driver requires from name",
},
"No Password": {
mail.Config{
URL: "https://smtp.example.com",
FromAddress: "hello@gophers.com",
FromName: "name",
},
"driver requires a password",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewSMTP(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestSMTP_Send() {
tt := map[string]struct {
input *mail.Transmission
send smtpSendFunc
want interface{}
}{
"Success": {
Trans,
func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return nil
},
mail.Response{
StatusCode: 200,
Message: "Email sent successfully",
},
},
"With Attachment": {
TransWithAttachment,
func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return nil
},
mail.Response{
StatusCode: 200,
Message: "Email sent successfully",
},
},
"Validation Failed": {
nil,
func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return nil
},
"can't validate a nil transmission",
},
"Send Error": {
Trans,
func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return errors.New("send error")
},
"send error",
},
}
for name, test := range tt {
t.Run(name, func() {
spark := smtpClient{
cfg: mail.Config{
FromAddress: "from",
},
send: test.send,
}
resp, err := spark.Send(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.Equal(test.want, resp)
})
}
}
func (t *DriversTestSuite) TestSMTP_Bytes() {
t.T().Skip()
m := smtpClient{}
got := m.bytes(&mail.Transmission{
Recipients: []string{"hello@gmail.com"},
Subject: "Subject",
HTML: "<h1>Hey!</h1>",
PlainText: "Hey!",
//Attachments: []mail.Attachment{
// {
// Filename: "test.jpg",
// Bytes: []byte("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg=="),
// },
//},
})
fmt.Println(string(got))
}
================================================
FILE: drivers/sparkpost.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"context"
"encoding/json"
"fmt"
"github.com/ainsleyclark/go-mail/internal/client"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"net/http"
"strings"
"time"
)
// postal represents the entity for sending mail via the
// Postal API.
//
// See:
// https://developers.sparkpost.com/api/
// https://developers.sparkpost.com/api/transmissions/#transmissions-create-a-transmission
type sparkPost struct {
cfg mail.Config
client client.Requester
}
const (
// sparkpostEndpoint defines the endpoint to POST to.
// See: https://www.sparkpost.com/api#/reference/transmissions
sparkpostEndpoint = "%s/api/v1/transmissions"
// sparkpostErrorMessage defines the message when an error occurred
// when sending mail via the SparkPost API.
sparkpostErrorMessage = "error sending transmission to SparkPost API"
)
// NewSparkPost creates a new SparkPost client. Configuration
// is validated before initialisation.
func NewSparkPost(cfg mail.Config) (mail.Mailer, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
return &sparkPost{
cfg: cfg,
client: client.New(cfg.Client),
}, nil
}
type (
// spTransmission is the JSON structure accepted by and returned
// from the SparkPost Transmissions API.
spTransmission struct {
ID string `json:"id,omitempty"`
State string `json:"state,omitempty"`
Options *spTransmissionOptions `json:"options,omitempty"`
Recipients []spRecipient `json:"recipients"`
CampaignID string `json:"campaign_id,omitempty"`
Description string `json:"description,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
SubstitutionData interface{} `json:"substitution_data,omitempty"`
ReturnPath string `json:"return_path,omitempty"`
Content spContent `json:"content"`
TotalRecipients *int `json:"total_recipients,omitempty"`
NumGenerated *int `json:"num_generated,omitempty"`
NumFailedGeneration *int `json:"num_failed_generation,omitempty"`
NumInvalidRecipients *int `json:"num_invalid_recipients,omitempty"`
}
// spTransmissionOptions specifies settings to apply to this Transmission.
// If not specified, and present in TmplOptions, those values will be used.
spTransmissionOptions struct {
OpenTracking *bool `json:"open_tracking,omitempty"`
ClickTracking *bool `json:"click_tracking,omitempty"`
Transactional *bool `json:"transactional,omitempty"`
StartTime *time.Time `json:"start_time,omitempty"`
Sandbox *bool `json:"sandbox,omitempty"`
SkipSuppression *bool `json:"skip_suppression,omitempty"`
IPPool string `json:"ip_pool,omitempty"`
InlineCSS *bool `json:"inline_css,omitempty"`
PerformSubstitutions *bool `json:"perform_substitutions,omitempty"`
}
// spContent is what will be sent to recipients.
// Knowledge of SparkPost's substitution/templating capabilities will come in handy here.
// https://www.sparkpost.com/api#/introduction/substitutions-reference
spContent struct {
HTML string `json:"html,omitempty"`
Text string `json:"text,omitempty"`
Subject string `json:"subject,omitempty"`
From spFrom `json:"from,omitempty"`
ReplyTo string `json:"reply_to,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
EmailRFC822 string `json:"email_rfc822,omitempty"`
Attachments []spAttachment `json:"attachments,omitempty"`
InlineImages []interface{} `json:"inline_images,omitempty"`
}
// spFrom describes the nested object way of specifying the `From` header.
// Content.From can be specified this way, or as a plain string.
spFrom struct {
Email string `json:"email"`
Name string `json:"name"`
}
// spResponse contains information about the last HTTP response from
// the SparkPost API.
//
// Example JSON Response:
// {"results":{"total_rejected_recipients":0,"total_accepted_recipients":1,"id":"7029753512321354395"}}
// {"errors":[{"message":"content.subject is a required field","code":"1400"}]}
spResponse struct {
Results map[string]interface{} `json:"results,omitempty"`
Errors []spError `json:"errors,omitempty"`
}
// spError mirrors the error format returned by SparkPost APIs.
spError struct {
Message string `json:"message"`
Code string `json:"code"`
Description string `json:"description"`
Part string `json:"part,omitempty"`
Line int `json:"line,omitempty"`
}
// spRecipient represents one email (you guessed it) recipient.
spRecipient struct {
Address spAddress `json:"address"`
ReturnPath string `json:"return_path,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
SubstitutionData interface{} `json:"substitution_data,omitempty"`
}
// spAddress describes the nested object way of specifying the
// Recipient's email address. Recipient.Address can also be
// a plain string.
spAddress struct {
Email string `json:"email"`
Name string `json:"name,omitempty"`
HeaderTo string `json:"header_to,omitempty"`
}
// spAttachment contains metadata and the contents of the
// file to attach.
spAttachment struct {
MIMEType string `json:"type"`
Filename string `json:"name"`
B64Data string `json:"data"`
}
)
func (r *spResponse) Unmarshal(buf []byte) error {
resp := &spResponse{}
err := json.Unmarshal(buf, resp)
if err != nil {
return err
}
*r = *resp
return nil
}
func (r *spResponse) CheckError(response *http.Response, buf []byte) error {
if len(r.Errors) == 0 {
return nil
}
if len(buf) == 0 {
return mail.ErrEmptyBody
}
return fmt.Errorf("%s - code: %s, message: %s", sparkpostErrorMessage, r.Errors[0].Code, r.Errors[0].Message)
}
func (r *spResponse) Meta() httputil.Meta {
m := httputil.Meta{
Message: "Successfully sent SparkPost email",
}
if val, ok := r.Results["id"]; ok {
m.ID = fmt.Sprintf("%v", val)
}
return m
}
func (d *sparkPost) Send(t *mail.Transmission) (mail.Response, error) {
err := t.Validate()
if err != nil {
return mail.Response{}, err
}
headerTo := strings.Join(t.Recipients, ",")
tx := spTransmission{
Content: spContent{
HTML: t.HTML,
Text: t.PlainText,
Subject: t.Subject,
From: spFrom{
Email: d.cfg.FromAddress,
Name: d.cfg.FromName,
},
ReplyTo: "",
Headers: make(map[string]string),
},
}
for _, r := range t.Recipients {
tx.Recipients = append(tx.Recipients, spRecipient{
Address: spAddress{Email: r, HeaderTo: headerTo},
})
}
if t.HasCC() {
for _, c := range t.CC {
tx.Recipients = append(tx.Recipients, spRecipient{
Address: spAddress{Email: c, HeaderTo: headerTo},
})
tx.Content.Headers["cc"] = strings.Join(t.CC, ",")
}
}
if t.HasBCC() {
for _, b := range t.BCC {
tx.Recipients = append(tx.Recipients, spRecipient{
Address: spAddress{Email: b, HeaderTo: headerTo},
})
}
}
if t.HasAttachments() {
for _, v := range t.Attachments {
tx.Content.Attachments = append(tx.Content.Attachments, spAttachment{
MIMEType: v.Mime(),
Filename: v.Filename,
B64Data: v.B64(),
})
}
}
tx.Content.Headers = t.Headers
pl, err := newJSONData(tx)
if err != nil {
return mail.Response{}, err
}
req := httputil.NewHTTPRequest(http.MethodPost, fmt.Sprintf(sparkpostEndpoint, d.cfg.URL))
req.AddHeader("Authorization", d.cfg.APIKey)
return d.client.Do(context.Background(), req, pl, &spResponse{})
}
================================================
FILE: drivers/sparkpost_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package drivers
import (
"fmt"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/client"
"github.com/ainsleyclark/go-mail/mail"
"log"
"net/http"
)
func ExampleNewSparkPost() {
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com", // Or https://api.sparkpost.com/api/v1
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
_, err := NewSparkPost(cfg)
if err != nil {
log.Fatalln(err)
}
}
func (t *DriversTestSuite) TestNewSparkPost() {
tt := map[string]struct {
input mail.Config
want interface{}
}{
"Success": {
mail.Config{
URL: "https://api.eu.sparkpost.com",
APIKey: "key",
FromAddress: "addr",
FromName: "name",
},
nil,
},
"Validation Failed": {
mail.Config{},
"driver requires from address",
},
"Error": {
mail.Config{
URL: "http://",
APIKey: "key",
FromAddress: "addr",
FromName: "name",
},
"API base url must be https!",
},
}
for name, test := range tt {
t.Run(name, func() {
got, err := NewSparkPost(test.input)
if err != nil {
t.Contains(err.Error(), test.want)
return
}
t.NotNil(got)
})
}
}
func (t *DriversTestSuite) TestSSparkPostResponse_Unmarshal() {
t.UtilTestUnmarshal(&spResponse{}, []byte(`{"results": {}}`))
}
func (t *DriversTestSuite) TestSparkPostResponse_CheckError() {
tt := map[string]struct {
input spResponse
response *http.Response
buf []byte
want error
}{
"Success": {
spResponse{Errors: nil},
&http.Response{StatusCode: http.StatusOK},
[]byte("test"),
nil,
},
"No Errors": {
spResponse{Errors: []spError{{Message: "message", Code: "code"}}},
&http.Response{StatusCode: http.StatusInternalServerError},
nil,
mail.ErrEmptyBody,
},
"Error": {
spResponse{Errors: []spError{{Message: "message", Code: "code"}}},
&http.Response{StatusCode: http.StatusInternalServerError},
[]byte("test"),
fmt.Errorf("%s - code: code, message: message", sparkpostErrorMessage),
},
}
for name, test := range tt {
t.Run(name, func() {
err := test.input.CheckError(test.response, test.buf)
if err != nil {
t.Contains(err.Error(), test.want.Error())
return
}
t.Equal(test.want, err)
})
}
}
func (t *DriversTestSuite) TestSparkPostResponse_Meta() {
d := &spResponse{
Results: map[string]interface{}{"id": "10"},
Errors: nil,
}
t.UtilTestMeta(d, "Successfully sent SparkPost email", "10")
}
func (t *DriversTestSuite) TestSparkPost_Send() {
t.UtilTestSend(func(m *mocks.Requester) mail.Mailer {
return &sparkPost{cfg: Comfig, client: m}
}, true)
}
================================================
FILE: examples/attachments.go
================================================
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"io/ioutil"
"log"
)
// Attachments example for Go Mail
func Attachments() {
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSparkPost(cfg)
if err != nil {
log.Fatalln(err)
}
image, err := ioutil.ReadFile("gopher.jpg")
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
Attachments: []mail.Attachment{
{
Filename: "gopher.jpg",
Bytes: image,
},
},
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/mailgun.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// MailGun example for Go Mail
func MailGun() {
cfg := mail.Config{
URL: "my-url",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Domain: "my-domain",
}
mailer, err := drivers.NewMailGun(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/postal.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// Postal example for Go Mail
func Postal() {
cfg := mail.Config{
URL: "https://postal.example.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewPostal(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/postmark.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// Postmark example for Go Mail
func Postmark() {
cfg := mail.Config{
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewPostmark(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/sendgrid.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// SendGrid example for Go Mail
func SendGrid() {
cfg := mail.Config{
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSendGrid(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/smtp.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// SMTP example for Go Mail
func SMTP() {
cfg := mail.Config{
URL: "smtp.gmail.com",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
Password: "my-password",
Port: 587,
}
mailer, err := drivers.NewSMTP(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: examples/sparkpost.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"log"
)
// Sparkpost example for Go Mail
func Sparkpost() {
cfg := mail.Config{
URL: "https://api.eu.sparkpost.com",
APIKey: "my-key",
FromAddress: "hello@gophers.com",
FromName: "Gopher",
}
mailer, err := drivers.NewSparkPost(cfg)
if err != nil {
log.Fatalln(err)
}
tx := &mail.Transmission{
Recipients: []string{"hello@gophers.com"},
Subject: "My email",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "plain text",
}
result, err := mailer.Send(tx)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", result)
}
================================================
FILE: go.mod
================================================
module github.com/ainsleyclark/go-mail
go 1.18
require (
github.com/joho/godotenv v1.4.0
github.com/stretchr/testify v1.9.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: internal/client/client.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"context"
"fmt"
"github.com/ainsleyclark/go-mail/internal/errors"
"github.com/ainsleyclark/go-mail/internal/httputil"
"github.com/ainsleyclark/go-mail/mail"
"io"
"net/http"
"strings"
"time"
)
// Requester defines the method used for interacting with
// a Mailable API.
type Requester interface {
// Do accepts a message, url endpoint and optional Headers to POST data
// to a drivers API.
// Returns an error if data could not be marshalled/unmarshalled
// or if the request could not be processed.
Do(ctx context.Context, r *httputil.Request, payload httputil.Payload, responder httputil.Responder) (mail.Response, error)
}
// New creates a new Client with a stdlib http.Client.
func New(client *http.Client) *Client {
if client == nil {
client = &http.Client{
Timeout: Timeout,
}
}
return &Client{
Client: client,
bodyReader: io.ReadAll,
}
}
const (
// Timeout is the amount of time to wait before
// a mail request is cancelled.
Timeout = time.Second * 10
)
// Client defines a http.Client to interact with the mail
// drivers API's. It acts as a reusable helper to send
// data to the drivers endpoints.
type Client struct {
Client *http.Client
bodyReader func(r io.Reader) ([]byte, error)
}
// Do accepts a message, Request and a Payload to POST data
// to a drivers API.
// Logs Curl output if mail.debug is set to true.
//
// Returns an error if data could not be marshalled/unmarshalled
// or if the request could not be processed.
func (c *Client) Do(ctx context.Context, r *httputil.Request, payload httputil.Payload, responder httputil.Responder) (mail.Response, error) {
const op = "Client.Do"
req, err := c.makeRequest(ctx, r, payload)
if err != nil {
return mail.Response{}, err
}
resp, err := c.Client.Do(req)
if err != nil {
return mail.Response{}, &errors.Error{Code: errors.API, Message: "Error doing request", Operation: op, Err: err}
}
defer resp.Body.Close()
response := mail.Response{
StatusCode: resp.StatusCode,
}
buf, err := c.bodyReader(resp.Body)
if err != nil {
return response, &errors.Error{Code: errors.INTERNAL, Message: "Error reading response body", Operation: op, Err: err}
}
response.Body = buf
err = responder.Unmarshal(buf)
if err != nil {
return response, &errors.Error{Code: errors.INVALID, Message: "Error unmarshalling response error", Operation: op, Err: err}
}
err = responder.CheckError(resp, buf)
if err != nil {
return response, &errors.Error{Code: errors.API, Message: "Error performing mail request", Operation: op, Err: err}
}
meta := responder.Meta()
return mail.Response{
StatusCode: resp.StatusCode,
Body: buf,
Headers: resp.Header,
ID: meta.ID,
Message: meta.Message,
}, nil
}
// makeRequest creates a stdlib http.Request.
// Content-Type, BasicAuth and headers are attached to the request.
// Returns an error if the request could not be created.
func (c *Client) makeRequest(ctx context.Context, r *httputil.Request, payload httputil.Payload) (*http.Request, error) {
const op = "Client.MakeRequest"
var body io.Reader
if payload != nil {
b, err := payload.Buffer()
if err != nil {
return nil, err
}
body = b
}
req, err := http.NewRequest(r.Method, r.URL, body)
if err != nil {
return nil, &errors.Error{Code: errors.INVALID, Message: "Error creating http request", Operation: op, Err: err}
}
if mail.Debug {
fmt.Println(c.curlString(req, payload))
}
req = req.WithContext(ctx)
if payload != nil && payload.ContentType() != "" {
req.Header.Add("Content-Type", payload.ContentType())
}
if r.BasicAuthUser != "" && r.BasicAuthPassword != "" {
req.SetBasicAuth(r.BasicAuthUser, r.BasicAuthPassword)
}
for header, value := range r.Headers {
req.Header.Add(header, value)
}
return req, nil
}
// curlString constructs a string used for posting the
// request via Curl.
func (c *Client) curlString(req *http.Request, p httputil.Payload) string {
parts := []string{"curl", "-i", "-X", req.Method, req.URL.String()}
for key, value := range req.Header {
parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, value[0]))
}
if p != nil {
for key, value := range p.Values() {
parts = append(parts, fmt.Sprintf(" -F %s='%s'", key, value))
}
}
return strings.Join(parts, " ")
}
================================================
FILE: internal/client/client_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"bytes"
"context"
"github.com/ainsleyclark/go-mail/internal/errors"
"github.com/ainsleyclark/go-mail/internal/httputil"
mocks "github.com/ainsleyclark/go-mail/internal/mocks/httputil"
"github.com/ainsleyclark/go-mail/mail"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func TestNewClient(t *testing.T) {
got := New(nil)
assert.NotNil(t, got.bodyReader)
c := &http.Client{}
withClient := New(c)
assert.Equal(t, withClient.Client, c)
}
func TestClient_Do(t *testing.T) {
successHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("buf"))
assert.NoError(t, err)
}
tt := map[string]struct {
input *httputil.Request
handler http.HandlerFunc
responder func(m *mocks.Responder)
bodyReader func(r io.Reader) ([]byte, error)
want interface{}
}{
"Success": {
handler: successHandler,
responder: func(m *mocks.Responder) {
m.On("Unmarshal", mock.Anything).
Return(nil)
m.On("CheckError", mock.Anything, []byte("buf")).
Return(nil)
m.On("Meta", mock.Anything).
Return(httputil.Meta{Message: "message", ID: "10"})
},
bodyReader: io.ReadAll,
want: mail.Response{
StatusCode: http.StatusOK,
Body: []byte("buf"),
Headers: nil,
ID: "10",
Message: "message",
},
},
"Bad Request": {
input: &httputil.Request{URL: "@#@#$$%$"},
want: "Error creating http request",
},
"Do Error": {
input: &httputil.Request{URL: "wrong"},
want: "Error doing request",
},
"Body Read Error": {
handler: successHandler,
bodyReader: func(r io.Reader) ([]byte, error) {
return nil, errors.New("body read error")
},
want: "Error reading response body",
},
"Unmarshal Error": {
handler: successHandler,
responder: func(m *mocks.Responder) {
m.On("Unmarshal", mock.Anything).
Return(errors.New("unmarshal error"))
},
bodyReader: io.ReadAll,
want: "Error unmarshalling response error",
},
"Responder Error": {
handler: successHandler,
responder: func(m *mocks.Responder) {
m.On("Unmarshal", mock.Anything).
Return(nil)
m.On("CheckError", mock.Anything, []byte("buf")).
Return(errors.New("response error"))
},
bodyReader: io.ReadAll,
want: "Error performing mail request",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
server := httptest.NewServer(test.handler)
defer server.Close()
if test.input == nil {
test.input = &httputil.Request{}
}
if test.input.URL == "" {
test.input.URL = server.URL
}
responder := &mocks.Responder{}
if test.responder != nil {
test.responder(responder)
}
c := Client{
Client: server.Client(),
bodyReader: test.bodyReader,
}
got, err := c.Do(context.Background(), test.input, nil, responder)
if err != nil {
assert.Contains(t, errors.Message(err), test.want)
return
}
want := test.want.(mail.Response)
assert.Equal(t, want.StatusCode, got.StatusCode)
assert.Equal(t, want.Body, got.Body)
assert.NotEmpty(t, got.Headers)
assert.Equal(t, want.ID, got.ID)
assert.Equal(t, want.Message, got.Message)
})
}
}
func TestClient_MakeRequest(t *testing.T) {
uri, err := url.Parse("https://gomail.example.com")
assert.NoError(t, err)
tt := map[string]struct {
request *httputil.Request
payload func(m *mocks.Payload)
want interface{}
}{
"Success": {
&httputil.Request{
Method: http.MethodPost,
URL: "https://gomail.example.com",
BasicAuthUser: "user",
BasicAuthPassword: "password",
Headers: map[string]string{"header": "Value"},
},
func(m *mocks.Payload) {
m.On("Buffer").
Return(&bytes.Buffer{}, nil)
m.On("ContentType").
Return(httputil.JSONContentType)
m.On("Values").
Return(map[string]string{"key": "value"})
},
&http.Request{
Method: http.MethodPost,
URL: uri,
Header: map[string][]string{
"Authorization": {"Basic dXNlcjpwYXNzd29yZA=="},
"Content-Type": {httputil.JSONContentType},
"Header": {"Value"},
},
},
},
"Buffer Error": {
&httputil.Request{},
func(m *mocks.Payload) {
m.On("Buffer").
Return(nil, &errors.Error{Message: "buffer error"})
},
"buffer error",
},
"Request Error": {
&httputil.Request{
URL: "@#@#$$%$",
},
func(m *mocks.Payload) {
m.On("Buffer").
Return(&bytes.Buffer{}, nil)
},
"Error creating http request",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
defer func() { mail.Debug = false }()
mail.Debug = true
c := New(nil)
mock := &mocks.Payload{}
if test.payload != nil {
test.payload(mock)
}
request, err := c.makeRequest(context.Background(), test.request, mock)
if err != nil {
assert.Contains(t, errors.Message(err), test.want)
return
}
want := test.want.(*http.Request)
assert.Equal(t, want.Method, request.Method)
assert.Equal(t, want.URL, request.URL)
assert.Equal(t, want.Header, request.Header)
})
}
}
func TestClient_CurlString(t *testing.T) {
uri, err := url.Parse("https://gomail.example.com")
assert.NoError(t, err)
req := &http.Request{
Method: http.MethodGet,
URL: uri,
Header: map[string][]string{"header": {"value"}},
}
payload := mocks.Payload{}
payload.On("Values").
Return(map[string]string{"key": "value"})
c := Client{}
got := c.curlString(req, &payload)
want := "curl -i -X GET https://gomail.example.com -H \"header: value\" -F key='value'"
assert.Equal(t, want, got)
}
================================================
FILE: internal/client/util.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
// Is2XX returns true if the provided HTTP response code is
// in the range 200-299.
func Is2XX(code int) bool {
if code < 300 && code >= 200 {
return true
}
return false
}
================================================
FILE: internal/client/util_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
func TestIs2XX(t *testing.T) {
tt := map[string]struct {
input int
want bool
}{
"< 200": {
http.StatusContinue,
false,
},
"200": {
http.StatusOK,
true,
},
"300 >": {
http.StatusMultipleChoices,
false,
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
got := Is2XX(test.input)
assert.Equal(t, test.want, got)
})
}
}
================================================
FILE: internal/errors/errors.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
"bytes"
"errors"
"fmt"
"github.com/ainsleyclark/go-mail/mail"
)
// Application error codes.
const (
// CONFLICT - An action cannot be performed.
CONFLICT = "conflict"
// INTERNAL - Error within Go Mail
INTERNAL = "internal" // Internal error
// INVALID - Validation failed
INVALID = "invalid" // Validation failed
// API - Error in the http request.
API = "api"
// Prefix is the string prefixed to an error message.
Prefix = "go-mail"
// GlobalError is a general message when no error message
// has been found.
GlobalError = "An error has occurred."
)
// Error defines a standard application error.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Operation string `json:"operation"`
Err error `json:"error"`
}
// Error returns the string representation of the error
// message.
func (e *Error) Error() string {
var buf bytes.Buffer
// Print the prefix
fmt.Fprintf(&buf, "%s: ", Prefix)
// Print the current operation in our stack, if any.
if e.Operation != "" && mail.Debug {
fmt.Fprintf(&buf, "%s: ", e.Operation)
}
// If wrapping an error, print its Error() message.
// Otherwise, print the error code & message.
if e.Err != nil {
buf.WriteString(e.Err.Error())
} else {
if e.Code != "" {
_, _ = fmt.Fprintf(&buf, "<%s> ", e.Code)
}
buf.WriteString(e.Message)
}
return buf.String()
}
// Code returns the code of the root error, if available.
// Otherwise, returns INTERNAL.
func Code(err error) string {
if err == nil {
return ""
} else if e, ok := err.(*Error); ok && e.Code != "" {
return e.Code
} else if ok && e.Err != nil {
return Code(e.Err)
}
return INTERNAL
}
// Message returns the human-readable message of the error,
// if available. Otherwise, returns a generic error
// message.
func Message(err error) string {
if err == nil {
return ""
} else if e, ok := err.(*Error); ok && e.Message != "" {
return e.Message
} else if ok && e.Err != nil {
return Message(e.Err)
}
return GlobalError
}
// ToError Returns a Go Mail error from input. If The type
// is not of type Error, nil will be returned.
func ToError(err interface{}) *Error {
switch v := err.(type) {
case *Error:
return v
case Error:
return &v
case error:
return &Error{Err: fmt.Errorf(v.Error())}
case string:
return &Error{Err: fmt.Errorf(v)}
default:
return nil
}
}
// New is a wrapper for the stdlib new function.
func New(text string) error {
return errors.New(text)
}
================================================
FILE: internal/errors/errors_test.go
================================================
package errors
import (
"fmt"
"github.com/ainsleyclark/go-mail/mail"
"github.com/stretchr/testify/assert"
"testing"
)
func TestError_Error(t *testing.T) {
tt := map[string]struct {
input *Error
debug bool
want string
}{
"Normal": {
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
false,
fmt.Sprintf("%s: err", Prefix),
},
"Debug": {
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
true,
fmt.Sprintf("%s: op: err", Prefix),
},
"Nil Operation": {
&Error{Code: INTERNAL, Message: "test", Operation: "", Err: fmt.Errorf("err")},
false,
fmt.Sprintf("%s: err", Prefix),
},
"Nil Err": {
&Error{Code: INTERNAL, Message: "test", Operation: "", Err: nil},
false,
fmt.Sprintf("%s: <internal> test", Prefix),
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
defer func() { mail.Debug = false }()
mail.Debug = test.debug
assert.Equal(t, test.want, test.input.Error())
})
}
}
func TestError_Code(t *testing.T) {
tt := map[string]struct {
input error
want string
}{
"Normal": {
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
"internal",
},
"Nil Input": {
nil,
"",
},
"Nil Code": {
&Error{Code: "", Message: "test", Operation: "op", Err: fmt.Errorf("err")},
"internal",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.want, Code(test.input))
})
}
}
func Test_Message(t *testing.T) {
tt := map[string]struct {
input error
want string
}{
"Normal": {
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
"test",
},
"Nil Input": {
nil,
"",
},
"Nil Message": {
&Error{Code: "", Message: "", Operation: "op", Err: fmt.Errorf("err")},
GlobalError,
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.want, Message(test.input))
})
}
}
func TestError_ToError(t *testing.T) {
tt := map[string]struct {
input interface{}
want *Error
}{
"Pointer": {
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
},
"Non Pointer": {
Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
&Error{Code: INTERNAL, Message: "test", Operation: "op", Err: fmt.Errorf("err")},
},
"Error": {
fmt.Errorf("err"),
&Error{Err: fmt.Errorf("err")},
},
"String": {
"err",
&Error{Err: fmt.Errorf("err")},
},
"Default": {
nil,
nil,
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.want, ToError(test.input))
})
}
}
func TestNew(t *testing.T) {
want := fmt.Errorf("error")
got := New("error")
assert.Errorf(t, want, got.Error())
}
================================================
FILE: internal/httputil/payload.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
import (
"bytes"
"encoding/json"
"fmt"
"github.com/ainsleyclark/go-mail/internal/errors"
"io"
"mime/multipart"
)
// Payload defines the methods used for creating HTTP payload
// helper.
type Payload interface {
// Buffer returns the byte buffer used for making the
// HTTP Request
Buffer() (*bytes.Buffer, error)
// ContentType returns the `Content-Type` header used for
// making the HTTP Request
ContentType() string
// Values returns a map of key - value pairs used for testing
// and debugging.
Values() map[string]string
}
const (
// JSONContentType is the Content-Type header for
// JSON payloads.
JSONContentType = "application/json"
)
// JSONData defines the payload for JSON types.
type JSONData struct {
original interface{}
values map[string]interface{}
}
// NewJSONData creates a new JSON Data Payload type.
// It adds a struct type to the JSON Data payload.
// Returns an error if the struct could not be marshalled or unmarshalled.
func NewJSONData(obj interface{}) (*JSONData, error) {
const op = "HTTPUtil.NewJSONData"
buf, err := json.Marshal(obj)
if err != nil {
return nil, &errors.Error{Code: errors.INTERNAL, Message: "Error marshalling payload", Operation: op, Err: err}
}
m := make(map[string]interface{})
err = json.Unmarshal(buf, &m)
if err != nil {
return nil, &errors.Error{Code: errors.INTERNAL, Message: "Error unmarshalling payload", Operation: op, Err: err}
}
return &JSONData{
original: obj,
values: m,
}, nil
}
// Buffer returns the byte buffer for making the request.
func (j *JSONData) Buffer() (*bytes.Buffer, error) {
const op = "JSONData.Buffer"
buf, err := json.Marshal(j.values)
if err != nil {
return nil, &errors.Error{Code: errors.INTERNAL, Message: "Error marshalling values", Operation: op, Err: err}
}
return bytes.NewBuffer(buf), nil
}
// ContentType returns the `Content-Type` header.
func (j *JSONData) ContentType() string {
return JSONContentType
}
// Values returns a map of key - value pairs used for testing
// and debugging.
func (j *JSONData) Values() map[string]string {
m := make(map[string]string)
for key, value := range j.values {
m[key] = fmt.Sprintf("%v", value)
}
return m
}
// FormData defines the payload for URL encoded types.
type FormData struct {
contentType string
values map[string]string
buffers []keyNameBuff
}
// keyNameBuff defines the buffer for multipart attachments.
type keyNameBuff struct {
key string
name string
value []byte
}
// NewFormData creates a new Form Data Payload type.
func NewFormData() *FormData {
return &FormData{}
}
// AddValue adds a key - value string pair to the Payload.
func (f *FormData) AddValue(key, value string) {
if len(f.values) == 0 {
f.values = make(map[string]string)
}
f.values[key] = value
}
// AddBuffer adds a file buffer to the Payload with a filename.
func (f *FormData) AddBuffer(key, fileName string, buff []byte) {
f.buffers = append(f.buffers, keyNameBuff{
key: key,
name: fileName,
value: buff,
})
}
var newWriter = multipart.NewWriter
// Buffer returns the byte buffer for making the request.
func (f *FormData) Buffer() (*bytes.Buffer, error) {
const op = "FormData.Buffer"
data := &bytes.Buffer{}
writer := newWriter(data)
defer writer.Close()
for key, val := range f.values {
if tmp, err := writer.CreateFormField(key); err == nil {
tmp.Write([]byte(val)) // nolint
} else {
return nil, &errors.Error{Code: errors.INTERNAL, Message: "Error creating form field", Operation: op, Err: err}
}
}
for _, buff := range f.buffers {
if tmp, err := writer.CreateFormFile(buff.key, buff.name); err == nil {
r := bytes.NewReader(buff.value)
io.Copy(tmp, r) // nolint
} else {
return nil, &errors.Error{Code: errors.INTERNAL, Message: "Error creating form file", Operation: op, Err: err}
}
}
f.contentType = writer.FormDataContentType()
return data, nil
}
// ContentType returns the `Content-Type` header.
func (f *FormData) ContentType() string {
if f.contentType == "" {
f.Buffer() // nolint
}
return f.contentType
}
// Values returns a map of key - value pairs used for testing
// and debugging.
func (f *FormData) Values() map[string]string {
return f.values
}
================================================
FILE: internal/httputil/payload_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
import (
"github.com/ainsleyclark/go-mail/internal/errors"
"github.com/stretchr/testify/assert"
"io"
"mime/multipart"
"testing"
)
func TestNewJSONData(t *testing.T) {
tt := map[string]struct {
input interface{}
want interface{}
}{
"Success": {
map[string]interface{}{"test": 1},
map[string]interface{}{"test": float64(1)},
},
"Marshal Error": {
map[string]interface{}{"test": make(chan struct{})},
"Error marshalling payload",
},
"Unmarshal Error": {
1,
"Error unmarshalling payload",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
pl, err := NewJSONData(test.input)
if err != nil {
assert.Contains(t, errors.Message(err), test.want)
return
}
assert.Equal(t, test.want, pl.values)
assert.NotNil(t, pl.original)
})
}
}
func TestJSONData_Buffer(t *testing.T) {
tt := map[string]struct {
input JSONData
want interface{}
}{
"Success": {
JSONData{values: map[string]interface{}{"test": 1}},
`{"test":1}`,
},
"Marshal Error": {
JSONData{values: map[string]interface{}{"test": make(chan struct{})}},
"unsupported type",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
got, err := test.input.Buffer()
if err != nil {
assert.Contains(t, err.Error(), test.want)
return
}
assert.Equal(t, test.want, got.String())
})
}
}
func TestJsonData_ContentType(t *testing.T) {
pl := JSONData{}
got := pl.ContentType()
assert.Equal(t, JSONContentType, got)
}
func TestJSONData_Values(t *testing.T) {
pl := JSONData{values: map[string]interface{}{"test": 1}}
got := pl.Values()
want := map[string]string{"test": "1"}
assert.Equal(t, want, got)
}
func TestFormData_AddValue(t *testing.T) {
pl := NewFormData()
pl.AddValue("key", "value")
want := map[string]string{"key": "value"}
assert.Equal(t, want, pl.values)
}
func TestFormData_AddBuffer(t *testing.T) {
pl := NewFormData()
pl.AddBuffer("key", "file", []byte("value"))
want := []keyNameBuff{
{key: "key", name: "file", value: []byte("value")},
}
assert.Equal(t, want, pl.buffers)
}
type mockWriterError struct{}
func (m *mockWriterError) Write(p []byte) (n int, err error) {
return 0, errors.New("write error")
}
func TestFormData_Buffer(t *testing.T) {
tt := map[string]struct {
input FormData
writer func(w io.Writer) *multipart.Writer
want interface{}
}{
"Success": {
FormData{
values: map[string]string{
"key": "value",
},
buffers: []keyNameBuff{
{key: "key", name: "file", value: []byte("value")},
},
},
multipart.NewWriter,
"Content-Disposition",
},
"Value Error": {
FormData{
values: map[string]string{"key": "value"},
},
func(w io.Writer) *multipart.Writer {
return multipart.NewWriter(&mockWriterError{})
},
"write error",
},
"Buffer Error": {
FormData{
buffers: []keyNameBuff{
{key: "key", name: "file", value: []byte("value")},
},
},
func(w io.Writer) *multipart.Writer {
return multipart.NewWriter(&mockWriterError{})
},
"write error",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
orig := newWriter
defer func() { newWriter = orig }()
newWriter = test.writer
got, err := test.input.Buffer()
if err != nil {
assert.Contains(t, err.Error(), test.want)
return
}
assert.Contains(t, got.String(), test.want)
})
}
}
func TestFormData_ContentType(t *testing.T) {
pl := FormData{values: map[string]string{"test": "1"}}
got := pl.ContentType()
want := "multipart/form-data"
assert.Contains(t, got, want)
}
func TestFormData_Values(t *testing.T) {
pl := FormData{values: map[string]string{"test": "1"}}
got := pl.Values()
want := map[string]string{"test": "1"}
assert.Equal(t, want, got)
}
================================================
FILE: internal/httputil/request.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
// A Request represents an HTTP request received by a server
// or to be sent by a client. It is an extension of the
// std http.Request for Go Mail.
type Request struct {
Method string
URL string
Headers map[string]string
BasicAuthUser string
BasicAuthPassword string
}
// NewHTTPRequest returns a new Request given a method and URL.
func NewHTTPRequest(method, url string) *Request {
return &Request{
Method: method,
URL: url,
}
}
// AddHeader adds the key, value pair to the request headers.
func (r *Request) AddHeader(name, value string) {
if r.Headers == nil {
r.Headers = make(map[string]string)
}
r.Headers[name] = value
}
// SetBasicAuth sets the request's Authorization header to use HTTP
// Basic Authentication with the provided username and password.
//
// With HTTP Basic Authentication the provided username and password
// are not encrypted.
func (r *Request) SetBasicAuth(user, password string) {
r.BasicAuthUser = user
r.BasicAuthPassword = password
}
================================================
FILE: internal/httputil/request_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
import (
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
func TestNewHTTPRequest(t *testing.T) {
req := NewHTTPRequest(http.MethodGet, "https://gomail.example.com")
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "https://gomail.example.com", req.URL)
}
func TestRequest_AddHeader(t *testing.T) {
req := NewHTTPRequest(http.MethodGet, "https://gomail.example.com")
req.AddHeader("header", "value")
want := map[string]string{"header": "value"}
assert.Equal(t, want, req.Headers)
}
func TestRequest_SetBasicAuth(t *testing.T) {
req := NewHTTPRequest(http.MethodGet, "https://gomail.example.com")
req.SetBasicAuth("user", "pass")
assert.Equal(t, "user", req.BasicAuthUser)
assert.Equal(t, "pass", req.BasicAuthPassword)
}
================================================
FILE: internal/httputil/response.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
import "net/http"
// Responder defines the methods used for a response back
// from a mailer's API.
type Responder interface {
Unmarshal(buf []byte) error
CheckError(response *http.Response, buf []byte) error
Meta() Meta
}
// Meta defines the data used for creating a mail.Response.
type Meta struct {
Message string
ID string
}
================================================
FILE: internal/mime/mime.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mime
import (
"bytes"
"net/http"
)
const (
// sniffLength is the amount of bytes to read to
// detect the MIME type.
sniffLength uint32 = 512
)
// DetectBuffer returns the MIME type found from the provided byte slice.
//
// The result is always a valid MIME type, with application/octet-stream
// returned when identification failed.
// Uses http.DetectContentType with a layer for SVG detection.
func DetectBuffer(buf []byte) string {
header := make([]byte, sniffLength)
copy(header, buf)
// Detect for SVGs
// See https://github.com/golang/go/issues/15888
if bytes.Contains(header, []byte("<svg")) {
return "image/svg+xml"
}
return http.DetectContentType(header)
}
================================================
FILE: internal/mime/mime_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mime
import (
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
func TestDetectBuffer(t *testing.T) {
tt := map[string]struct {
input string
want string
}{
"PNG": {
"gopher.png",
"image/png",
},
"JPG": {
"gopher.jpg",
"image/jpeg",
},
"SVG": {
"gopher.svg",
"image/svg+xml",
},
}
for name, test := range tt {
t.Run(name, func(t *testing.T) {
wd, err := os.Getwd()
assert.NoError(t, err)
path := filepath.Join(filepath.Join(wd, "../../testdata"), test.input)
file, err := os.ReadFile(path)
assert.NoError(t, err)
got := DetectBuffer(file)
assert.Equal(t, test.want, got)
})
}
}
================================================
FILE: internal/mocks/client/Requester.go
================================================
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
context "context"
httputil "github.com/ainsleyclark/go-mail/internal/httputil"
mail "github.com/ainsleyclark/go-mail/mail"
mock "github.com/stretchr/testify/mock"
)
// Requester is an autogenerated mock type for the Requester type
type Requester struct {
mock.Mock
}
// Do provides a mock function with given fields: ctx, r, payload, responder
func (_m *Requester) Do(ctx context.Context, r *httputil.Request, payload httputil.Payload, responder httputil.Responder) (mail.Response, error) {
ret := _m.Called(ctx, r, payload, responder)
var r0 mail.Response
if rf, ok := ret.Get(0).(func(context.Context, *httputil.Request, httputil.Payload, httputil.Responder) mail.Response); ok {
r0 = rf(ctx, r, payload, responder)
} else {
r0 = ret.Get(0).(mail.Response)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *httputil.Request, httputil.Payload, httputil.Responder) error); ok {
r1 = rf(ctx, r, payload, responder)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
================================================
FILE: internal/mocks/drivers/smtpSendFunc.go
================================================
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
smtp "net/smtp"
mock "github.com/stretchr/testify/mock"
)
// smtpSendFunc is an autogenerated mock type for the smtpSendFunc type
type smtpSendFunc struct {
mock.Mock
}
// Execute provides a mock function with given fields: addr, a, from, to, msg
func (_m *smtpSendFunc) Execute(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
ret := _m.Called(addr, a, from, to, msg)
var r0 error
if rf, ok := ret.Get(0).(func(string, smtp.Auth, string, []string, []byte) error); ok {
r0 = rf(addr, a, from, to, msg)
} else {
r0 = ret.Error(0)
}
return r0
}
================================================
FILE: internal/mocks/httputil/Payload.go
================================================
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
bytes "bytes"
mock "github.com/stretchr/testify/mock"
)
// Payload is an autogenerated mock type for the Payload type
type Payload struct {
mock.Mock
}
// Buffer provides a mock function with given fields:
func (_m *Payload) Buffer() (*bytes.Buffer, error) {
ret := _m.Called()
var r0 *bytes.Buffer
if rf, ok := ret.Get(0).(func() *bytes.Buffer); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bytes.Buffer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ContentType provides a mock function with given fields:
func (_m *Payload) ContentType() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Values provides a mock function with given fields:
func (_m *Payload) Values() map[string]string {
ret := _m.Called()
var r0 map[string]string
if rf, ok := ret.Get(0).(func() map[string]string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
return r0
}
================================================
FILE: internal/mocks/httputil/Responder.go
================================================
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
http "net/http"
httputil "github.com/ainsleyclark/go-mail/internal/httputil"
mock "github.com/stretchr/testify/mock"
)
// Responder is an autogenerated mock type for the Responder type
type Responder struct {
mock.Mock
}
// CheckError provides a mock function with given fields: response, buf
func (_m *Responder) CheckError(response *http.Response, buf []byte) error {
ret := _m.Called(response, buf)
var r0 error
if rf, ok := ret.Get(0).(func(*http.Response, []byte) error); ok {
r0 = rf(response, buf)
} else {
r0 = ret.Error(0)
}
return r0
}
// Meta provides a mock function with given fields:
func (_m *Responder) Meta() httputil.Meta {
ret := _m.Called()
var r0 httputil.Meta
if rf, ok := ret.Get(0).(func() httputil.Meta); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(httputil.Meta)
}
return r0
}
// Unmarshal provides a mock function with given fields: buf
func (_m *Responder) Unmarshal(buf []byte) error {
ret := _m.Called(buf)
var r0 error
if rf, ok := ret.Get(0).(func([]byte) error); ok {
r0 = rf(buf)
} else {
r0 = ret.Error(0)
}
return r0
}
================================================
FILE: internal/mocks/mail/Mailer.go
================================================
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
mail "github.com/ainsleyclark/go-mail/mail"
mock "github.com/stretchr/testify/mock"
)
// Mailer is an autogenerated mock type for the Mailer type
type Mailer struct {
mock.Mock
}
// Send provides a mock function with given fields: t
func (_m *Mailer) Send(t *mail.Transmission) (mail.Response, error) {
ret := _m.Called(t)
var r0 mail.Response
if rf, ok := ret.Get(0).(func(*mail.Transmission) mail.Response); ok {
r0 = rf(t)
} else {
r0 = ret.Get(0).(mail.Response)
}
var r1 error
if rf, ok := ret.Get(1).(func(*mail.Transmission) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
================================================
FILE: mail/attachments.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"encoding/base64"
"github.com/ainsleyclark/go-mail/internal/mime"
)
// Attachment defines an email attachment for Go Mail.
// It contains useful information for sending files via
// the mail driver.
type Attachment struct {
Filename string
Bytes []byte
}
// Mime returns the mime type of the byte data.
func (a Attachment) Mime() string {
return mime.DetectBuffer(a.Bytes)
}
// B64 returns the base 64 encoding of the attachment.
func (a Attachment) B64() string {
return base64.StdEncoding.EncodeToString(a.Bytes)
}
================================================
FILE: mail/attachments_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import "fmt"
func ExampleAttachment_Mime() {
svg := `
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`
a := Attachment{
Filename: "circle.svg",
Bytes: []byte(svg),
}
fmt.Println(a.Mime())
// Output: image/svg+xml
}
func (t *MailTestSuite) TestAttachment_Mime() {
tt := map[string]struct {
input string
want string
}{
"PNG": {
PNGName,
"image/png",
},
"JPG": {
JPGName,
"image/jpeg",
},
"SVG": {
SVGName,
"image/svg+xml",
},
}
for name, test := range tt {
t.Run(name, func() {
a := t.Attachment(test.input)
got := a.Mime()
t.Equal(test.want, got)
})
}
}
func ExampleAttachment_B64() {
svg := `
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`
a := Attachment{
Filename: "circle.svg",
Bytes: []byte(svg),
}
fmt.Println(a.B64())
// Output: Cjxzdmcgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPgogIDxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjQwIiBzdHJva2U9ImdyZWVuIiBzdHJva2Utd2lkdGg9IjQiIGZpbGw9InllbGxvdyIgLz4KPC9zdmc+
}
func (t *MailTestSuite) TestAttachment_B64() {
a := Attachment{
Bytes: []byte("hello"),
}
got := a.B64()
t.Equal("aGVsbG8=", got)
}
================================================
FILE: mail/config.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"errors"
"net/http"
)
// Config represents the configuration passed when a new
// client is constructed. Dependant on what driver is used,
// different options are required to be present.
type Config struct {
URL string
APIKey string
Domain string
FromAddress string
FromName string
Password string
Port int
Client *http.Client
}
// Validate runs sanity checks of a Config struct.
// This is run before a new client is created
// to ensure there are no invalid API
// calls.
func (c *Config) Validate() error {
if c.FromAddress == "" {
return errors.New("driver requires from address")
}
if c.FromName == "" {
return errors.New("driver requires from name")
}
if c.APIKey == "" {
return errors.New("driver requires api key")
}
return nil
}
================================================
FILE: mail/config_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"errors"
"fmt"
)
func ExampleConfig_Validate() {
cfg := Config{}
fmt.Println(cfg.Validate())
// Output: driver requires from address
}
func (t *MailTestSuite) TestConfig_Validate() {
tt := map[string]struct {
input Config
want error
}{
"Success": {
Config{
APIKey: "key",
FromAddress: "hello@test.com",
FromName: "Test",
},
nil,
},
"No From Address": {
Config{
APIKey: "key",
FromName: "Test",
},
errors.New("driver requires from address"),
},
"No From Name": {
Config{
APIKey: "key",
FromAddress: "hello@test.com",
},
errors.New("driver requires from name"),
},
"No Key": {
Config{
FromAddress: "hello@test.com",
FromName: "Test",
},
errors.New("driver requires api key"),
},
}
for name, test := range tt {
t.Run(name, func() {
got := test.input.Validate()
t.Equal(test.want, got)
})
}
}
================================================
FILE: mail/mail.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import "errors"
var (
// Debug - Set true to write the HTTP requests in curl to stdout.
// Additional information will also be displayed in the errors such as
// method operations.
Debug = false
// ErrEmptyBody is returned by Send when there is nobody attached to the
// request.
ErrEmptyBody = errors.New("error, empty body")
)
// Mailer defines the sender for go-mail returning a
// Response or error when an email is sent.
//
// Below is an example of creating and sending a transmission:
//
// cfg := mail.Config{
// URL: "https://api.eu.sparkpost.com",
// APIKey: "my-key",
// FromAddress: "hello@gophers.com",
// FromName: "Gopher",
// }
//
// mailer, err := drivers.NewSparkPost(cfg)
// if err != nil {
// log.Fatalln(err)
// }
//
// tx := &mail.Transmission{
// Recipients: []string{"hello@gophers.com"},
// Subject: "My email",
// HTML: "<h1>Hello from Go Mail!</h1>",
// }
//
// result, err := mailer.Send(tx)
// if err != nil {
// log.Fatalln(err)
// }
//
// fmt.Printf("%+v\n", result)
type Mailer interface {
// Send accepts a mail.Transmission to send an email through a particular
// driver/provider. Transmissions will be validated before sending.
//
// A mail.Response or an error will be returned. In some circumstances
// the body and status code will be attached to the response for debugging.
Send(t *Transmission) (Response, error)
}
================================================
FILE: mail/mail_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/stretchr/testify/suite"
"os"
"path/filepath"
"testing"
)
// MailTestSuite defines the helper used for mail
// testing.
type MailTestSuite struct {
suite.Suite
base string
}
// Assert testing has begun.
func TestMail(t *testing.T) {
suite.Run(t, new(MailTestSuite))
}
// Assigns test base.
func (t *MailTestSuite) SetupSuite() {
wd, err := os.Getwd()
t.NoError(err)
t.base = wd
}
const (
// DataPath defines where the test data resides.
DataPath = "testdata"
// PNGName defines the PNG name for testing.
PNGName = "gopher.png"
// JPGName defines the JPG name for testing.
JPGName = "gopher.jpg"
// SVGName defines the SVG name testing.
SVGName = "gopher.svg"
)
// Returns a PNG attachment for testing.
func (t *MailTestSuite) Attachment(name string) Attachment {
path := filepath.Join(filepath.Dir(t.base), DataPath, name)
file, err := os.ReadFile(path)
if err != nil {
t.Fail("error getting attachment with the path: "+path, err)
}
return Attachment{
Filename: name,
Bytes: file,
}
}
================================================
FILE: mail/response.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import "net/http"
// Response represents the data passed back from a successful transmission.
type Response struct {
StatusCode int // e.g. 200
Body []byte // e.g. {"result: success"}
Headers http.Header // e.g. map[X-Ratelimit-Limit:[600]]
ID string // e.g "100"
Message string // e.g "Email sent successfully"
}
================================================
FILE: mail/transmissions.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"errors"
)
// Transmission represents the JSON structure accepted by
// and returned from the driver's API. Recipients,
// HTML and a subject is required to send the
// email.
type Transmission struct {
Recipients []string
CC []string
BCC []string
Subject string
HTML string
PlainText string
Attachments []Attachment
Headers map[string]string
}
// Validate runs sanity checks of a Transmission struct.
// This is run before any email sending to ensure
// there are no invalid API calls.
func (t *Transmission) Validate() error {
if t == nil {
return errors.New("can't validate a nil transmission")
}
if len(t.Recipients) == 0 {
return errors.New("transmission requires recipients")
}
if t.Subject == "" {
return errors.New("transmission requires a subject")
}
if t.HTML == "" {
return errors.New("transmission requires html content")
}
return nil
}
// HasCC determines if there are any CC recipients
// attached to the transmission.
func (t *Transmission) HasCC() bool {
return len(t.CC) != 0
}
// HasBCC determines if there are any BCC recipients
// attached to the transmission.
func (t *Transmission) HasBCC() bool {
return len(t.BCC) != 0
}
// HasAttachments determines if there are any attachments in
// the transmission.
func (t Transmission) HasAttachments() bool {
return len(t.Attachments) != 0
}
================================================
FILE: mail/transmissions_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"errors"
"fmt"
)
func ExampleTransmission_Validate() {
t := Transmission{}
fmt.Println(t.Validate())
// Output: transmission requires recipients
}
func (t *MailTestSuite) TestTransmission_Validate() {
tt := map[string]struct {
input *Transmission
want error
}{
"Success": {
&Transmission{
Recipients: []string{"hello@test.com"},
Subject: "subject",
HTML: "<h1>Hello</h1>",
},
nil,
},
"Nil": {
nil,
errors.New("can't validate a nil transmission"),
},
"No Recipients": {
&Transmission{
HTML: "html",
Subject: "subject",
},
errors.New("transmission requires recipients"),
},
"No Subject": {
&Transmission{
Recipients: []string{"hello@test.com"},
HTML: "html",
},
errors.New("transmission requires a subject"),
},
"No HTML": {
&Transmission{
Recipients: []string{"hello@test.com"},
Subject: "subject",
},
errors.New("transmission requires html content"),
},
}
for name, test := range tt {
t.Run(name, func() {
got := test.input.Validate()
t.Equal(test.want, got)
})
}
}
func ExampleTransmission_HasCC() {
t := Transmission{
CC: []string{"cc@gophers.com"},
}
fmt.Println(t.HasCC())
// Output: true
}
func (t *MailTestSuite) TestConfig_HasCC() {
tt := map[string]struct {
input Transmission
want bool
}{
"With": {
Transmission{CC: []string{"hello@test.com"}},
true,
},
"Without": {
Transmission{},
false,
},
}
for name, test := range tt {
t.Run(name, func() {
got := test.input.HasCC()
t.Equal(test.want, got)
})
}
}
func ExampleTransmission_HasBCC() {
t := Transmission{
BCC: []string{"bcc@gophers.com"},
}
fmt.Println(t.HasBCC())
// Output: true
}
func (t *MailTestSuite) TestConfig_HasBCC() {
tt := map[string]struct {
input Transmission
want bool
}{
"With": {
Transmission{BCC: []string{"hello@test.com"}},
true,
},
"Without": {
Transmission{},
false,
},
}
for name, test := range tt {
t.Run(name, func() {
got := test.input.HasBCC()
t.Equal(test.want, got)
})
}
}
func ExampleTransmission_HasAttachments() {
t := Transmission{
Attachments: []Attachment{
{
Filename: "gopher.svg",
Bytes: []byte("svg"),
},
},
}
fmt.Println(t.HasAttachments())
// Output: true
}
func (t *MailTestSuite) TestTransmission_HasAttachments() {
tt := map[string]struct {
input Transmission
want bool
}{
"Exists": {
Transmission{
Attachments: []Attachment{{Filename: PNGName}},
},
true,
},
"Nil": {
Transmission{},
false,
},
}
for name, test := range tt {
t.Run(name, func() {
got := test.input.HasAttachments()
t.Equal(test.want, got)
})
}
}
================================================
FILE: mocks/client/Requester.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
context "context"
httputil "github.com/ainsleyclark/go-mail/internal/httputil"
mail "github.com/ainsleyclark/go-mail/mail"
mock "github.com/stretchr/testify/mock"
)
// Requester is an autogenerated mock type for the Requester type
type Requester struct {
mock.Mock
}
// Do provides a mock function with given fields: ctx, r, payload, responder
func (_m *Requester) Do(ctx context.Context, r *httputil.Request, payload httputil.Payload, responder httputil.Responder) (mail.Response, error) {
ret := _m.Called(ctx, r, payload, responder)
var r0 mail.Response
if rf, ok := ret.Get(0).(func(context.Context, *httputil.Request, httputil.Payload, httputil.Responder) mail.Response); ok {
r0 = rf(ctx, r, payload, responder)
} else {
r0 = ret.Get(0).(mail.Response)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *httputil.Request, httputil.Payload, httputil.Responder) error); ok {
r1 = rf(ctx, r, payload, responder)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
================================================
FILE: mocks/clientold/Requester.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
http "net/http"
mock "github.com/stretchr/testify/mock"
)
// Requester is an autogenerated mock type for the Requester type
type Requester struct {
mock.Mock
}
// Do provides a mock function with given fields: message, url, headers
func (_m *Requester) Do(message interface{}, url string, headers http.Header) ([]byte, *http.Response, error) {
ret := _m.Called(message, url, headers)
var r0 []byte
if rf, ok := ret.Get(0).(func(interface{}, string, http.Header) []byte); ok {
r0 = rf(message, url, headers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 *http.Response
if rf, ok := ret.Get(1).(func(interface{}, string, http.Header) *http.Response); ok {
r1 = rf(message, url, headers)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*http.Response)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(interface{}, string, http.Header) error); ok {
r2 = rf(message, url, headers)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
================================================
FILE: mocks/drivers/smtpSendFunc.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
smtp "net/smtp"
mock "github.com/stretchr/testify/mock"
)
// smtpSendFunc is an autogenerated mock type for the smtpSendFunc type
type smtpSendFunc struct {
mock.Mock
}
// Execute provides a mock function with given fields: addr, a, from, to, msg
func (_m *smtpSendFunc) Execute(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
ret := _m.Called(addr, a, from, to, msg)
var r0 error
if rf, ok := ret.Get(0).(func(string, smtp.Auth, string, []string, []byte) error); ok {
r0 = rf(addr, a, from, to, msg)
} else {
r0 = ret.Error(0)
}
return r0
}
================================================
FILE: mocks/httputil/Payload.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
bytes "bytes"
mock "github.com/stretchr/testify/mock"
)
// Payload is an autogenerated mock type for the Payload type
type Payload struct {
mock.Mock
}
// Buffer provides a mock function with given fields:
func (_m *Payload) Buffer() (*bytes.Buffer, error) {
ret := _m.Called()
var r0 *bytes.Buffer
if rf, ok := ret.Get(0).(func() *bytes.Buffer); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bytes.Buffer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ContentType provides a mock function with given fields:
func (_m *Payload) ContentType() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Values provides a mock function with given fields:
func (_m *Payload) Values() map[string]string {
ret := _m.Called()
var r0 map[string]string
if rf, ok := ret.Get(0).(func() map[string]string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
return r0
}
================================================
FILE: mocks/httputil/Responder.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
http "net/http"
httputil "github.com/ainsleyclark/go-mail/internal/httputil"
mock "github.com/stretchr/testify/mock"
)
// Responder is an autogenerated mock type for the Responder type
type Responder struct {
mock.Mock
}
// CheckError provides a mock function with given fields: response, buf
func (_m *Responder) CheckError(response *http.Response, buf []byte) error {
ret := _m.Called(response, buf)
var r0 error
if rf, ok := ret.Get(0).(func(*http.Response, []byte) error); ok {
r0 = rf(response, buf)
} else {
r0 = ret.Error(0)
}
return r0
}
// Meta provides a mock function with given fields:
func (_m *Responder) Meta() httputil.Meta {
ret := _m.Called()
var r0 httputil.Meta
if rf, ok := ret.Get(0).(func() httputil.Meta); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(httputil.Meta)
}
return r0
}
// Unmarshal provides a mock function with given fields: buf
func (_m *Responder) Unmarshal(buf []byte) error {
ret := _m.Called(buf)
var r0 error
if rf, ok := ret.Get(0).(func([]byte) error); ok {
r0 = rf(buf)
} else {
r0 = ret.Error(0)
}
return r0
}
================================================
FILE: mocks/mail/Mailer.go
================================================
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
mail "github.com/ainsleyclark/go-mail/mail"
mock "github.com/stretchr/testify/mock"
)
// Mailer is an autogenerated mock type for the Mailer type
type Mailer struct {
mock.Mock
}
// Send provides a mock function with given fields: t
func (_m *Mailer) Send(t *mail.Transmission) (mail.Response, error) {
ret := _m.Called(t)
var r0 mail.Response
if rf, ok := ret.Get(0).(func(*mail.Transmission) mail.Response); ok {
r0 = rf(t)
} else {
r0 = ret.Get(0).(mail.Response)
}
var r1 error
if rf, ok := ret.Get(1).(func(*mail.Transmission) error); ok {
r1 = rf(t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
================================================
FILE: tests/mail_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"fmt"
"github.com/ainsleyclark/go-mail/mail"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
const (
// DataPath defines where the test data resides.
DataPath = "testdata"
// PNGName defines the PNG name for testing.
PNGName = "gopher.png"
)
// Load the Env variables for testing.
func LoadEnv(t *testing.T) {
t.Helper()
wd, err := os.Getwd()
assert.NoError(t, err)
path := filepath.Join(filepath.Dir(wd), "/.env")
err = godotenv.Load(path)
if err != nil {
fmt.Println(err)
fmt.Printf("Error loading .env file with path: %s, using system defaults.\n", path)
}
}
// Returns a dummy transition for testing with an
// attachment.
func GetTransmission(t *testing.T) *mail.Transmission {
t.Helper()
wd, err := os.Getwd()
assert.NoError(t, err)
path := filepath.Join(filepath.Dir(wd), DataPath, PNGName)
file, err := os.ReadFile(path)
if err != nil {
t.Fatal("Error getting attachment with the path: "+path, err)
}
return &mail.Transmission{
Recipients: strings.Split(os.Getenv("EMAIL_TO"), ","),
CC: strings.Split(os.Getenv("EMAIL_CC"), ","),
BCC: strings.Split(os.Getenv("EMAIL_BCC"), ","),
Subject: "Test - Go Mail",
HTML: "<h1>Hello from Go Mail!</h1>",
PlainText: "Hello from Go Mail!",
Headers: map[string]string{
"X-Go-Mail": "Test",
},
Attachments: []mail.Attachment{
{
Filename: "gopher.png",
Bytes: file,
},
},
}
}
// UtilTestSend is a helper function for performing live mailing
// tests for the drivers.
func UtilTestSend(t *testing.T, fn func(cfg mail.Config) (mail.Mailer, error), cfg mail.Config, driver string) {
t.Helper()
tx := GetTransmission(t)
mailer, err := fn(cfg)
if err != nil {
t.Fatal("Error creating client: " + err.Error())
}
result, err := mailer.Send(tx)
if err != nil {
t.Fatalf("Error sending %s email: %s", strings.Title(driver), err.Error()) //nolint
}
// Print for sanity
fmt.Println(string(result.Body))
assert.InDelta(t, result.StatusCode, http.StatusOK, 299)
assert.NotEmpty(t, result.Message)
}
================================================
FILE: tests/mailgun_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"testing"
)
func Test_MailGun(t *testing.T) {
LoadEnv(t)
cfg := mail.Config{
URL: os.Getenv("MAILGUN_URL"),
APIKey: os.Getenv("MAILGUN_API_KEY"),
FromAddress: os.Getenv("MAILGUN_FROM_ADDRESS"),
FromName: os.Getenv("MAILGUN_FROM_NAME"),
Domain: os.Getenv("MAILGUN_DOMAIN"),
}
UtilTestSend(t, drivers.NewMailgun, cfg, "MailGun")
}
================================================
FILE: tests/postal_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"testing"
)
func Test_Postal(t *testing.T) {
LoadEnv(t)
cfg := mail.Config{
URL: os.Getenv("POSTAL_URL"),
APIKey: os.Getenv("POSTAL_API_KEY"),
FromAddress: os.Getenv("POSTAL_FROM_ADDRESS"),
FromName: os.Getenv("POSTAL_FROM_NAME"),
}
UtilTestSend(t, drivers.NewPostal, cfg, "Postal")
}
================================================
FILE: tests/postmark_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"testing"
)
func Test_Postmark(t *testing.T) {
LoadEnv(t)
cfg := mail.Config{
APIKey: os.Getenv("POSTMARK_API_KEY"),
FromAddress: os.Getenv("POSTMARK_FROM_ADDRESS"),
FromName: os.Getenv("POSTMARK_FROM_NAME"),
}
UtilTestSend(t, drivers.NewPostmark, cfg, "Postmark")
}
================================================
FILE: tests/sendgrid_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"testing"
)
func Test_SendGrid(t *testing.T) {
LoadEnv(t)
cfg := mail.Config{
APIKey: os.Getenv("SENDGRID_API_KEY"),
FromAddress: os.Getenv("SENDGRID_FROM_ADDRESS"),
FromName: os.Getenv("SENDGRID_FROM_NAME"),
}
UtilTestSend(t, drivers.NewSendGrid, cfg, "SendGrid")
}
================================================
FILE: tests/smtp_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"strconv"
"testing"
)
func Test_SMTP(t *testing.T) {
LoadEnv(t)
port, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
if err != nil {
t.Fatal("Error parsing SMTP port")
}
cfg := mail.Config{
URL: os.Getenv("SMTP_URL"),
FromAddress: os.Getenv("SMTP_FROM_ADDRESS"),
FromName: os.Getenv("SMTP_FROM_NAME"),
Password: os.Getenv("SMTP_PASSWORD"),
Port: port,
}
UtilTestSend(t, drivers.NewSMTP, cfg, "SMTP")
}
================================================
FILE: tests/sparkpost_test.go
================================================
// Copyright 2022 Ainsley Clark. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mail
import (
"github.com/ainsleyclark/go-mail/drivers"
"github.com/ainsleyclark/go-mail/mail"
"os"
"testing"
)
func Test_SparkPost(t *testing.T) {
LoadEnv(t)
cfg := mail.Config{
URL: os.Getenv("SPARKPOST_URL"),
APIKey: os.Getenv("SPARKPOST_API_KEY"),
FromAddress: os.Getenv("SPARKPOST_FROM_ADDRESS"),
FromName: os.Getenv("SPARKPOST_FROM_NAME"),
}
UtilTestSend(t, drivers.NewSparkPost, cfg, "SparkPost")
}
gitextract_8hbv64jc/
├── .editorconfig
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SECURITY.md
│ └── workflows/
│ ├── codeql-analysis.yml
│ ├── email.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── bin/
│ ├── tag.sh
│ └── tests.sh
├── drivers/
│ ├── drivers.go
│ ├── drivers_test.go
│ ├── mailgun.go
│ ├── mailgun_test.go
│ ├── postal.go
│ ├── postal_test.go
│ ├── postmark.go
│ ├── postmark_test.go
│ ├── sendgrid.go
│ ├── sendgrid_test.go
│ ├── smtp.go
│ ├── smtp_test.go
│ ├── sparkpost.go
│ └── sparkpost_test.go
├── examples/
│ ├── attachments.go
│ ├── mailgun.go
│ ├── postal.go
│ ├── postmark.go
│ ├── sendgrid.go
│ ├── smtp.go
│ └── sparkpost.go
├── go.mod
├── go.sum
├── internal/
│ ├── client/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── util.go
│ │ └── util_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ ├── httputil/
│ │ ├── payload.go
│ │ ├── payload_test.go
│ │ ├── request.go
│ │ ├── request_test.go
│ │ └── response.go
│ ├── mime/
│ │ ├── mime.go
│ │ └── mime_test.go
│ └── mocks/
│ ├── client/
│ │ └── Requester.go
│ ├── drivers/
│ │ └── smtpSendFunc.go
│ ├── httputil/
│ │ ├── Payload.go
│ │ └── Responder.go
│ └── mail/
│ └── Mailer.go
├── mail/
│ ├── attachments.go
│ ├── attachments_test.go
│ ├── config.go
│ ├── config_test.go
│ ├── mail.go
│ ├── mail_test.go
│ ├── response.go
│ ├── transmissions.go
│ └── transmissions_test.go
├── mocks/
│ ├── client/
│ │ └── Requester.go
│ ├── clientold/
│ │ └── Requester.go
│ ├── drivers/
│ │ └── smtpSendFunc.go
│ ├── httputil/
│ │ ├── Payload.go
│ │ └── Responder.go
│ └── mail/
│ └── Mailer.go
└── tests/
├── mail_test.go
├── mailgun_test.go
├── postal_test.go
├── postmark_test.go
├── sendgrid_test.go
├── smtp_test.go
└── sparkpost_test.go
SYMBOL INDEX (260 symbols across 60 files)
FILE: drivers/drivers_test.go
type DriversTestSuite (line 31) | type DriversTestSuite struct
method SetupSuite (line 42) | func (t *DriversTestSuite) SetupSuite() {
method Attachment (line 87) | func (t *DriversTestSuite) Attachment(name string) mail.Attachment {
method UtilTestUnmarshal (line 99) | func (t *DriversTestSuite) UtilTestUnmarshal(r httputil.Responder, buf...
method UtilTestMeta (line 107) | func (t *DriversTestSuite) UtilTestMeta(r httputil.Responder, message,...
method UtilTestSend (line 113) | func (t *DriversTestSuite) UtilTestSend(fn func(m *mocks.Requester) ma...
function TestMail (line 37) | func TestMail(t *testing.T) {
constant DataPath (line 50) | DataPath = "testdata"
FILE: drivers/mailgun.go
type mailGun (line 34) | type mailGun struct
method Send (line 101) | func (m *mailGun) Send(t *mail.Transmission) (mail.Response, error) {
constant mailgunEndpoint (line 41) | mailgunEndpoint = "/v3/%s/messages"
function NewMailgun (line 46) | func NewMailgun(cfg mail.Config) (mail.Mailer, error) {
type mailgunResponse (line 68) | type mailgunResponse struct
method Unmarshal (line 74) | func (r *mailgunResponse) Unmarshal(buf []byte) error {
method CheckError (line 84) | func (r *mailgunResponse) CheckError(response *http.Response, buf []by...
method Meta (line 94) | func (r *mailgunResponse) Meta() httputil.Meta {
FILE: drivers/mailgun_test.go
function ExampleNewMailgun (line 24) | func ExampleNewMailgun() {
method TestNewMailGun (line 39) | func (t *DriversTestSuite) TestNewMailGun() {
method TestMailgunResponse_Unmarshal (line 80) | func (t *DriversTestSuite) TestMailgunResponse_Unmarshal() {
method TestMailgunResponse_CheckError (line 84) | func (t *DriversTestSuite) TestMailgunResponse_CheckError() {
method TestMailgunResponse_Meta (line 120) | func (t *DriversTestSuite) TestMailgunResponse_Meta() {
method TestMailGun_Send (line 125) | func (t *DriversTestSuite) TestMailGun_Send() {
FILE: drivers/postal.go
type postal (line 33) | type postal struct
method Send (line 131) | func (d *postal) Send(t *mail.Transmission) (mail.Response, error) {
constant postalEndpoint (line 40) | postalEndpoint = "%s/api/v1/send/message"
constant postalErrorMessage (line 43) | postalErrorMessage = "error sending transmission to Postal API"
function NewPostal (line 48) | func NewPostal(cfg mail.Config) (mail.Mailer, error) {
type postalTransmission (line 61) | type postalTransmission struct
type postalAttachment (line 74) | type postalAttachment struct
type postalResponse (line 86) | type postalResponse struct
method Unmarshal (line 94) | func (r *postalResponse) Unmarshal(buf []byte) error {
method CheckError (line 104) | func (r *postalResponse) CheckError(response *http.Response, buf []byt...
method Meta (line 121) | func (r *postalResponse) Meta() httputil.Meta {
FILE: drivers/postal_test.go
function ExampleNewPostal (line 24) | func ExampleNewPostal() {
method TestNewPostal (line 38) | func (t *DriversTestSuite) TestNewPostal() {
method TestPostalResponse_Unmarshal (line 70) | func (t *DriversTestSuite) TestPostalResponse_Unmarshal() {
method TestPostalResponse_CheckError (line 74) | func (t *DriversTestSuite) TestPostalResponse_CheckError() {
method TestPostalResponse_Meta (line 113) | func (t *DriversTestSuite) TestPostalResponse_Meta() {
method TestPostal_Send (line 120) | func (t *DriversTestSuite) TestPostal_Send() {
FILE: drivers/postmark.go
type postmark (line 32) | type postmark struct
method Send (line 135) | func (d *postmark) Send(t *mail.Transmission) (mail.Response, error) {
constant postmarkEndpoint (line 39) | postmarkEndpoint = "https://api.postmarkapp.com/email"
constant postmarkErrorMessage (line 42) | postmarkErrorMessage = "error sending transmission to Postmark API"
function NewPostmark (line 47) | func NewPostmark(cfg mail.Config) (mail.Mailer, error) {
type postmarkTransmission (line 60) | type postmarkTransmission struct
type postmarkHeader (line 82) | type postmarkHeader struct
type postmarkAttachment (line 87) | type postmarkAttachment struct
type postmarkResponse (line 99) | type postmarkResponse struct
method Unmarshal (line 108) | func (r *postmarkResponse) Unmarshal(buf []byte) error {
method CheckError (line 118) | func (r *postmarkResponse) CheckError(response *http.Response, buf []b...
method Meta (line 128) | func (r *postmarkResponse) Meta() httputil.Meta {
FILE: drivers/postmark_test.go
function ExampleNewPostmark (line 24) | func ExampleNewPostmark() {
method TestNewPostmark (line 38) | func (t *DriversTestSuite) TestNewPostmark() {
method TestPostmarkResponse_Unmarshal (line 69) | func (t *DriversTestSuite) TestPostmarkResponse_Unmarshal() {
method TestPostmarkResponse_CheckError (line 73) | func (t *DriversTestSuite) TestPostmarkResponse_CheckError() {
method TestPostmarkResponse_Meta (line 112) | func (t *DriversTestSuite) TestPostmarkResponse_Meta() {
method TestPostmark_Send (line 117) | func (t *DriversTestSuite) TestPostmark_Send() {
FILE: drivers/sendgrid.go
type sendGrid (line 32) | type sendGrid struct
method Send (line 158) | func (d *sendGrid) Send(t *mail.Transmission) (mail.Response, error) {
constant sendGridEndpoint (line 40) | sendGridEndpoint = "https://api.sendgrid.com/v3/mail/send"
constant sendgridErrorMessage (line 43) | sendgridErrorMessage = "error sending transmission to SendGrid API"
function NewSendGrid (line 48) | func NewSendGrid(cfg mail.Config) (mail.Mailer, error) {
type sgTransmission (line 61) | type sgTransmission struct
type sgPersonalization (line 78) | type sgPersonalization struct
type sgEmail (line 92) | type sgEmail struct
type sgContent (line 97) | type sgContent struct
type sgAttachment (line 102) | type sgAttachment struct
type sgResponse (line 116) | type sgResponse struct
method Unmarshal (line 127) | func (r *sgResponse) Unmarshal(buf []byte) error {
method CheckError (line 140) | func (r *sgResponse) CheckError(response *http.Response, buf []byte) e...
method Meta (line 150) | func (r *sgResponse) Meta() httputil.Meta {
type sgError (line 120) | type sgError struct
FILE: drivers/sendgrid_test.go
function ExampleNewSendGrid (line 24) | func ExampleNewSendGrid() {
method TestNewSendGrid (line 37) | func (t *DriversTestSuite) TestNewSendGrid() {
method TestSendGridResponse_Unmarshal (line 69) | func (t *DriversTestSuite) TestSendGridResponse_Unmarshal() {
method TestSendGridResponse_CheckError (line 76) | func (t *DriversTestSuite) TestSendGridResponse_CheckError() {
method TestSendGridResponse_Meta (line 115) | func (t *DriversTestSuite) TestSendGridResponse_Meta() {
method TestSendGrid_Send (line 120) | func (t *DriversTestSuite) TestSendGrid_Send() {
FILE: drivers/smtp.go
type smtpClient (line 32) | type smtpClient struct
method Send (line 65) | func (m *smtpClient) Send(t *mail.Transmission) (mail.Response, error) {
method getTo (line 85) | func (m *smtpClient) getTo(t *mail.Transmission) []string {
method bytes (line 96) | func (m *smtpClient) bytes(t *mail.Transmission) []byte {
type smtpSendFunc (line 39) | type smtpSendFunc
function NewSMTP (line 43) | func NewSMTP(cfg mail.Config) (mail.Mailer, error) {
FILE: drivers/smtp_test.go
method TestNewSMTP (line 23) | func (t *DriversTestSuite) TestNewSMTP() {
method TestSMTP_Send (line 76) | func (t *DriversTestSuite) TestSMTP_Send() {
method TestSMTP_Bytes (line 136) | func (t *DriversTestSuite) TestSMTP_Bytes() {
FILE: drivers/sparkpost.go
type sparkPost (line 34) | type sparkPost struct
method Send (line 186) | func (d *sparkPost) Send(t *mail.Transmission) (mail.Response, error) {
constant sparkpostEndpoint (line 42) | sparkpostEndpoint = "%s/api/v1/transmissions"
constant sparkpostErrorMessage (line 45) | sparkpostErrorMessage = "error sending transmission to SparkPost API"
function NewSparkPost (line 50) | func NewSparkPost(cfg mail.Config) (mail.Mailer, error) {
type spTransmission (line 64) | type spTransmission struct
type spTransmissionOptions (line 82) | type spTransmissionOptions struct
type spContent (line 96) | type spContent struct
type spFrom (line 109) | type spFrom struct
type spResponse (line 119) | type spResponse struct
method Unmarshal (line 156) | func (r *spResponse) Unmarshal(buf []byte) error {
method CheckError (line 166) | func (r *spResponse) CheckError(response *http.Response, buf []byte) e...
method Meta (line 176) | func (r *spResponse) Meta() httputil.Meta {
type spError (line 124) | type spError struct
type spRecipient (line 132) | type spRecipient struct
type spAddress (line 142) | type spAddress struct
type spAttachment (line 149) | type spAttachment struct
FILE: drivers/sparkpost_test.go
function ExampleNewSparkPost (line 24) | func ExampleNewSparkPost() {
method TestNewSparkPost (line 38) | func (t *DriversTestSuite) TestNewSparkPost() {
method TestSSparkPostResponse_Unmarshal (line 79) | func (t *DriversTestSuite) TestSSparkPostResponse_Unmarshal() {
method TestSparkPostResponse_CheckError (line 83) | func (t *DriversTestSuite) TestSparkPostResponse_CheckError() {
method TestSparkPostResponse_Meta (line 122) | func (t *DriversTestSuite) TestSparkPostResponse_Meta() {
method TestSparkPost_Send (line 130) | func (t *DriversTestSuite) TestSparkPost_Send() {
FILE: examples/attachments.go
function Attachments (line 23) | func Attachments() {
FILE: examples/mailgun.go
function MailGun (line 24) | func MailGun() {
FILE: examples/postal.go
function Postal (line 24) | func Postal() {
FILE: examples/postmark.go
function Postmark (line 24) | func Postmark() {
FILE: examples/sendgrid.go
function SendGrid (line 24) | func SendGrid() {
FILE: examples/smtp.go
function SMTP (line 24) | func SMTP() {
FILE: examples/sparkpost.go
function Sparkpost (line 24) | func Sparkpost() {
FILE: internal/client/client.go
type Requester (line 30) | type Requester interface
function New (line 39) | func New(client *http.Client) *Client {
constant Timeout (line 54) | Timeout = time.Second * 10
type Client (line 60) | type Client struct
method Do (line 71) | func (c *Client) Do(ctx context.Context, r *httputil.Request, payload ...
method makeRequest (line 118) | func (c *Client) makeRequest(ctx context.Context, r *httputil.Request,...
method curlString (line 158) | func (c *Client) curlString(req *http.Request, p httputil.Payload) str...
FILE: internal/client/client_test.go
function TestNewClient (line 32) | func TestNewClient(t *testing.T) {
function TestClient_Do (line 40) | func TestClient_Do(t *testing.T) {
function TestClient_MakeRequest (line 150) | func TestClient_MakeRequest(t *testing.T) {
function TestClient_CurlString (line 231) | func TestClient_CurlString(t *testing.T) {
FILE: internal/client/util.go
function Is2XX (line 18) | func Is2XX(code int) bool {
FILE: internal/client/util_test.go
function TestIs2XX (line 22) | func TestIs2XX(t *testing.T) {
FILE: internal/errors/errors.go
constant CONFLICT (line 26) | CONFLICT = "conflict"
constant INTERNAL (line 28) | INTERNAL = "internal"
constant INVALID (line 30) | INVALID = "invalid"
constant API (line 32) | API = "api"
constant Prefix (line 34) | Prefix = "go-mail"
constant GlobalError (line 37) | GlobalError = "An error has occurred."
type Error (line 41) | type Error struct
method Error (line 50) | func (e *Error) Error() string {
function Code (line 77) | func Code(err error) string {
function Message (line 91) | func Message(err error) string {
function ToError (line 104) | func ToError(err interface{}) *Error {
function New (line 120) | func New(text string) error {
FILE: internal/errors/errors_test.go
function TestError_Error (line 10) | func TestError_Error(t *testing.T) {
function TestError_Code (line 47) | func TestError_Code(t *testing.T) {
function Test_Message (line 73) | func Test_Message(t *testing.T) {
function TestError_ToError (line 99) | func TestError_ToError(t *testing.T) {
function TestNew (line 133) | func TestNew(t *testing.T) {
FILE: internal/httputil/payload.go
type Payload (line 27) | type Payload interface
constant JSONContentType (line 42) | JSONContentType = "application/json"
type JSONData (line 46) | type JSONData struct
method Buffer (line 75) | func (j *JSONData) Buffer() (*bytes.Buffer, error) {
method ContentType (line 85) | func (j *JSONData) ContentType() string {
method Values (line 91) | func (j *JSONData) Values() map[string]string {
function NewJSONData (line 54) | func NewJSONData(obj interface{}) (*JSONData, error) {
type FormData (line 100) | type FormData struct
method AddValue (line 119) | func (f *FormData) AddValue(key, value string) {
method AddBuffer (line 127) | func (f *FormData) AddBuffer(key, fileName string, buff []byte) {
method Buffer (line 138) | func (f *FormData) Buffer() (*bytes.Buffer, error) {
method ContentType (line 168) | func (f *FormData) ContentType() string {
method Values (line 177) | func (f *FormData) Values() map[string]string {
type keyNameBuff (line 107) | type keyNameBuff struct
function NewFormData (line 114) | func NewFormData() *FormData {
FILE: internal/httputil/payload_test.go
function TestNewJSONData (line 24) | func TestNewJSONData(t *testing.T) {
function TestJSONData_Buffer (line 56) | func TestJSONData_Buffer(t *testing.T) {
function TestJsonData_ContentType (line 83) | func TestJsonData_ContentType(t *testing.T) {
function TestJSONData_Values (line 89) | func TestJSONData_Values(t *testing.T) {
function TestFormData_AddValue (line 96) | func TestFormData_AddValue(t *testing.T) {
function TestFormData_AddBuffer (line 103) | func TestFormData_AddBuffer(t *testing.T) {
type mockWriterError (line 112) | type mockWriterError struct
method Write (line 114) | func (m *mockWriterError) Write(p []byte) (n int, err error) {
function TestFormData_Buffer (line 118) | func TestFormData_Buffer(t *testing.T) {
function TestFormData_ContentType (line 175) | func TestFormData_ContentType(t *testing.T) {
function TestFormData_Values (line 182) | func TestFormData_Values(t *testing.T) {
FILE: internal/httputil/request.go
type Request (line 19) | type Request struct
method AddHeader (line 36) | func (r *Request) AddHeader(name, value string) {
method SetBasicAuth (line 48) | func (r *Request) SetBasicAuth(user, password string) {
function NewHTTPRequest (line 28) | func NewHTTPRequest(method, url string) *Request {
FILE: internal/httputil/request_test.go
function TestNewHTTPRequest (line 22) | func TestNewHTTPRequest(t *testing.T) {
function TestRequest_AddHeader (line 28) | func TestRequest_AddHeader(t *testing.T) {
function TestRequest_SetBasicAuth (line 35) | func TestRequest_SetBasicAuth(t *testing.T) {
FILE: internal/httputil/response.go
type Responder (line 20) | type Responder interface
type Meta (line 27) | type Meta struct
FILE: internal/mime/mime.go
constant sniffLength (line 24) | sniffLength uint32 = 512
function DetectBuffer (line 32) | func DetectBuffer(buf []byte) string {
FILE: internal/mime/mime_test.go
function TestDetectBuffer (line 23) | func TestDetectBuffer(t *testing.T) {
FILE: internal/mocks/client/Requester.go
type Requester (line 15) | type Requester struct
method Do (line 20) | func (_m *Requester) Do(ctx context.Context, r *httputil.Request, payl...
FILE: internal/mocks/drivers/smtpSendFunc.go
type smtpSendFunc (line 12) | type smtpSendFunc struct
method Execute (line 17) | func (_m *smtpSendFunc) Execute(addr string, a smtp.Auth, from string,...
FILE: internal/mocks/httputil/Payload.go
type Payload (line 12) | type Payload struct
method Buffer (line 17) | func (_m *Payload) Buffer() (*bytes.Buffer, error) {
method ContentType (line 40) | func (_m *Payload) ContentType() string {
method Values (line 54) | func (_m *Payload) Values() map[string]string {
FILE: internal/mocks/httputil/Responder.go
type Responder (line 13) | type Responder struct
method CheckError (line 18) | func (_m *Responder) CheckError(response *http.Response, buf []byte) e...
method Meta (line 32) | func (_m *Responder) Meta() httputil.Meta {
method Unmarshal (line 46) | func (_m *Responder) Unmarshal(buf []byte) error {
FILE: internal/mocks/mail/Mailer.go
type Mailer (line 11) | type Mailer struct
method Send (line 16) | func (_m *Mailer) Send(t *mail.Transmission) (mail.Response, error) {
FILE: mail/attachments.go
type Attachment (line 24) | type Attachment struct
method Mime (line 30) | func (a Attachment) Mime() string {
method B64 (line 35) | func (a Attachment) B64() string {
FILE: mail/attachments_test.go
function ExampleAttachment_Mime (line 18) | func ExampleAttachment_Mime() {
method TestAttachment_Mime (line 33) | func (t *MailTestSuite) TestAttachment_Mime() {
function ExampleAttachment_B64 (line 61) | func ExampleAttachment_B64() {
method TestAttachment_B64 (line 76) | func (t *MailTestSuite) TestAttachment_B64() {
FILE: mail/config.go
type Config (line 24) | type Config struct
method Validate (line 39) | func (c *Config) Validate() error {
FILE: mail/config_test.go
function ExampleConfig_Validate (line 21) | func ExampleConfig_Validate() {
method TestConfig_Validate (line 27) | func (t *MailTestSuite) TestConfig_Validate() {
FILE: mail/mail.go
type Mailer (line 57) | type Mailer interface
FILE: mail/mail_test.go
type MailTestSuite (line 25) | type MailTestSuite struct
method SetupSuite (line 36) | func (t *MailTestSuite) SetupSuite() {
method Attachment (line 54) | func (t *MailTestSuite) Attachment(name string) Attachment {
function TestMail (line 31) | func TestMail(t *testing.T) {
constant DataPath (line 44) | DataPath = "testdata"
constant PNGName (line 46) | PNGName = "gopher.png"
constant JPGName (line 48) | JPGName = "gopher.jpg"
constant SVGName (line 50) | SVGName = "gopher.svg"
FILE: mail/response.go
type Response (line 19) | type Response struct
FILE: mail/transmissions.go
type Transmission (line 24) | type Transmission struct
method Validate (line 38) | func (t *Transmission) Validate() error {
method HasCC (line 60) | func (t *Transmission) HasCC() bool {
method HasBCC (line 66) | func (t *Transmission) HasBCC() bool {
method HasAttachments (line 72) | func (t Transmission) HasAttachments() bool {
FILE: mail/transmissions_test.go
function ExampleTransmission_Validate (line 21) | func ExampleTransmission_Validate() {
method TestTransmission_Validate (line 27) | func (t *MailTestSuite) TestTransmission_Validate() {
function ExampleTransmission_HasCC (line 75) | func ExampleTransmission_HasCC() {
method TestConfig_HasCC (line 83) | func (t *MailTestSuite) TestConfig_HasCC() {
function ExampleTransmission_HasBCC (line 106) | func ExampleTransmission_HasBCC() {
method TestConfig_HasBCC (line 114) | func (t *MailTestSuite) TestConfig_HasBCC() {
function ExampleTransmission_HasAttachments (line 137) | func ExampleTransmission_HasAttachments() {
method TestTransmission_HasAttachments (line 150) | func (t *MailTestSuite) TestTransmission_HasAttachments() {
FILE: mocks/client/Requester.go
type Requester (line 15) | type Requester struct
method Do (line 20) | func (_m *Requester) Do(ctx context.Context, r *httputil.Request, payl...
FILE: mocks/clientold/Requester.go
type Requester (line 12) | type Requester struct
method Do (line 17) | func (_m *Requester) Do(message interface{}, url string, headers http....
FILE: mocks/drivers/smtpSendFunc.go
type smtpSendFunc (line 12) | type smtpSendFunc struct
method Execute (line 17) | func (_m *smtpSendFunc) Execute(addr string, a smtp.Auth, from string,...
FILE: mocks/httputil/Payload.go
type Payload (line 12) | type Payload struct
method Buffer (line 17) | func (_m *Payload) Buffer() (*bytes.Buffer, error) {
method ContentType (line 40) | func (_m *Payload) ContentType() string {
method Values (line 54) | func (_m *Payload) Values() map[string]string {
FILE: mocks/httputil/Responder.go
type Responder (line 13) | type Responder struct
method CheckError (line 18) | func (_m *Responder) CheckError(response *http.Response, buf []byte) e...
method Meta (line 32) | func (_m *Responder) Meta() httputil.Meta {
method Unmarshal (line 46) | func (_m *Responder) Unmarshal(buf []byte) error {
FILE: mocks/mail/Mailer.go
type Mailer (line 11) | type Mailer struct
method Send (line 16) | func (_m *Mailer) Send(t *mail.Transmission) (mail.Response, error) {
FILE: tests/mail_test.go
constant DataPath (line 30) | DataPath = "testdata"
constant PNGName (line 32) | PNGName = "gopher.png"
function LoadEnv (line 36) | func LoadEnv(t *testing.T) {
function GetTransmission (line 52) | func GetTransmission(t *testing.T) *mail.Transmission {
function UtilTestSend (line 86) | func UtilTestSend(t *testing.T, fn func(cfg mail.Config) (mail.Mailer, e...
FILE: tests/mailgun_test.go
function Test_MailGun (line 23) | func Test_MailGun(t *testing.T) {
FILE: tests/postal_test.go
function Test_Postal (line 23) | func Test_Postal(t *testing.T) {
FILE: tests/postmark_test.go
function Test_Postmark (line 23) | func Test_Postmark(t *testing.T) {
FILE: tests/sendgrid_test.go
function Test_SendGrid (line 23) | func Test_SendGrid(t *testing.T) {
FILE: tests/smtp_test.go
function Test_SMTP (line 24) | func Test_SMTP(t *testing.T) {
FILE: tests/sparkpost_test.go
function Test_SparkPost (line 23) | func Test_SparkPost(t *testing.T) {
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (191K chars).
[
{
"path": ".editorconfig",
"chars": 175,
"preview": "root = true\n\n[*]\nend_of_line = lf\nindent_size = 4\nindent_style = tab\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 5202,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 1616,
"preview": "# Contributing\n\nHello, we are very happy you decided to contribute to Go Mail. But before you start with your contributi"
},
{
"path": ".github/FUNDING.yml",
"chars": 23,
"preview": "github: [ainsleyclark]\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 602,
"preview": "---\nname: Bug Report\nabout: Create a report to help us improve Go Mail\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Describ"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 589,
"preview": "---\nname: Feature request\nabout: Suggest an idea for Go Mail\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feature r"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1303,
"preview": "# Description\n\nPlease include a summary of the change and which issue is fixed. Please also include relevant motivation "
},
{
"path": ".github/SECURITY.md",
"chars": 142,
"preview": "# Security Policy\n\n## Reporting an Issue\n\nIf you need to report a security issue please email the author [here](mailto:i"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2319,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/email.yml",
"chars": 2409,
"preview": "name: Email\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n schedule:\n - cron: '30 1 1,1"
},
{
"path": ".github/workflows/test.yml",
"chars": 806,
"preview": "name: Test\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n workflow_dispatch:\n\njobs:\n\n bui"
},
{
"path": ".gitignore",
"chars": 377,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
},
{
"path": ".golangci.yml",
"chars": 208,
"preview": "linters:\n enable:\n - gofmt\n - govet\n - gocyclo\n - ineffassign\n - thelper\n - tparallel\n - unconvert"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2022 Ainsley Clark\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "Makefile",
"chars": 1215,
"preview": "# Setup\nsetup:\n\tsudo chmod +x ./bin/tests.sh\n\tgo mod tidy\n.PHONY: setup\n\n# Run gofmt\nformat:\n\tgo fmt ./...\n.PHONY: forma"
},
{
"path": "README.md",
"chars": 11142,
"preview": "<div align=\"center\">\n<img height=\"300\" src=\"res/logos/go-mail.svg?size=new2\" alt=\"Go Mail Logo\" />\n\n[;\n// you may not use this file except in compliance wit"
},
{
"path": "examples/mailgun.go",
"chars": 1300,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "examples/postal.go",
"chars": 1289,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "examples/postmark.go",
"chars": 1250,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "examples/sendgrid.go",
"chars": 1250,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "examples/smtp.go",
"chars": 1296,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "examples/sparkpost.go",
"chars": 1300,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "go.mod",
"chars": 322,
"preview": "module github.com/ainsleyclark/go-mail\n\ngo 1.18\n\nrequire (\n\tgithub.com/joho/godotenv v1.4.0\n\tgithub.com/stretchr/testify"
},
{
"path": "go.sum",
"chars": 1838,
"preview": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1"
},
{
"path": "internal/client/client.go",
"chars": 4935,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/client/client_test.go",
"chars": 6391,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/client/util.go",
"chars": 800,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/client/util_test.go",
"chars": 1078,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/errors/errors.go",
"chars": 3120,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/errors/errors_test.go",
"chars": 2932,
"preview": "package errors\n\nimport (\n\t\"fmt\"\n\t\"github.com/ainsleyclark/go-mail/mail\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\""
},
{
"path": "internal/httputil/payload.go",
"chars": 4864,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/httputil/payload_test.go",
"chars": 4428,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/httputil/request.go",
"chars": 1657,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/httputil/request_test.go",
"chars": 1384,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/httputil/response.go",
"chars": 965,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/mime/mime.go",
"chars": 1300,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/mime/mime_test.go",
"chars": 1285,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "internal/mocks/client/Requester.go",
"chars": 1077,
"preview": "// Code generated by mockery v2.9.4. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\thttputil \"github.com/ain"
},
{
"path": "internal/mocks/drivers/smtpSendFunc.go",
"chars": 659,
"preview": "// Code generated by mockery v2.9.4. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tsmtp \"net/smtp\"\n\n\tmock \"github.com/stretchr/"
},
{
"path": "internal/mocks/httputil/Payload.go",
"chars": 1216,
"preview": "// Code generated by mockery v2.9.4. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tbytes \"bytes\"\n\n\tmock \"github.com/stretchr/te"
},
{
"path": "internal/mocks/httputil/Responder.go",
"chars": 1172,
"preview": "// Code generated by mockery v2.9.4. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\thttp \"net/http\"\n\n\thttputil \"github.com/ainsl"
},
{
"path": "internal/mocks/mail/Mailer.go",
"chars": 702,
"preview": "// Code generated by mockery v2.9.4. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmail \"github.com/ainsleyclark/go-mail/mail\"\n"
},
{
"path": "mail/attachments.go",
"chars": 1158,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/attachments_test.go",
"chars": 1888,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/config.go",
"chars": 1429,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/config_test.go",
"chars": 1552,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/mail.go",
"chars": 2067,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/mail_test.go",
"chars": 1663,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/response.go",
"chars": 983,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/transmissions.go",
"chars": 2008,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mail/transmissions_test.go",
"chars": 3365,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "mocks/client/Requester.go",
"chars": 1081,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\thttputil \"github.com"
},
{
"path": "mocks/clientold/Requester.go",
"chars": 1071,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\thttp \"net/http\"\n\n\tmock \"github.com/stret"
},
{
"path": "mocks/drivers/smtpSendFunc.go",
"chars": 663,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tsmtp \"net/smtp\"\n\n\tmock \"github.com/stret"
},
{
"path": "mocks/httputil/Payload.go",
"chars": 1220,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tbytes \"bytes\"\n\n\tmock \"github.com/stretch"
},
{
"path": "mocks/httputil/Responder.go",
"chars": 1176,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\thttp \"net/http\"\n\n\thttputil \"github.com/a"
},
{
"path": "mocks/mail/Mailer.go",
"chars": 706,
"preview": "// Code generated by mockery v0.0.0-dev. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmail \"github.com/ainsleyclark/go-mail/ma"
},
{
"path": "tests/mail_test.go",
"chars": 2759,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/mailgun_test.go",
"chars": 1084,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/postal_test.go",
"chars": 1033,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/postmark_test.go",
"chars": 1005,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/sendgrid_test.go",
"chars": 1005,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/smtp_test.go",
"chars": 1160,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "tests/sparkpost_test.go",
"chars": 1054,
"preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
}
]
About this extraction
This page contains the full source code of the ainsleyclark/go-mail GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (167.5 KB), approximately 47.4k tokens, and a symbol index with 260 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.