Full Code of ainsleyclark/go-mail for AI

main 1c0a1e6ec5da cached
81 files
167.5 KB
47.4k tokens
260 symbols
1 requests
Download .txt
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" />

[![made-with-Go](https://img.shields.io/badge/Made%20with-Go-1f425f.svg)](http://golang.org)
[![Go Report Card](https://goreportcard.com/badge/github.com/ainsleyclark/go-mail)](https://goreportcard.com/report/github.com/ainsleyclark/go-mail)
[![Test](https://github.com/ainsleyclark/go-mail/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/ainsleyclark/go-mail/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/ainsleyclark/go-mail/branch/main/graph/badge.svg?token=1ZI9R34CHQ)](https://codecov.io/gh/ainsleyclark/go-mail)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/ainsleyclark/go-mail)
[![Twitter Handle](https://img.shields.io/twitter/follow/ainsleydev)](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")
}
Download .txt
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
Download .txt
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[![made-with-Go](htt"
  },
  {
    "path": "bin/tag.sh",
    "chars": 376,
    "preview": "#!/bin/bash\n#\n# tag.sh\n#\n\n# Set variables\nversion=$1\nmessage=$2\n\n# Check version is not empty\nif [[ $version == \"\" ]]\n  "
  },
  {
    "path": "bin/tests.sh",
    "chars": 546,
    "preview": "#!/usr/bin/bash\n\n# Shell script for executing tests based on input.\n# Usage:\n# ./tests.sh for all drivers\n# ./tests.sh s"
  },
  {
    "path": "drivers/drivers.go",
    "chars": 937,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/drivers_test.go",
    "chars": 4888,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/mailgun.go",
    "chars": 3634,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/mailgun_test.go",
    "chars": 3038,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/postal.go",
    "chars": 4958,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/postal_test.go",
    "chars": 3004,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/postmark.go",
    "chars": 5340,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/postmark_test.go",
    "chars": 2912,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/sendgrid.go",
    "chars": 7392,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/sendgrid_test.go",
    "chars": 2958,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/smtp.go",
    "chars": 4604,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/smtp_test.go",
    "chars": 3492,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/sparkpost.go",
    "chars": 8582,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "drivers/sparkpost_test.go",
    "chars": 3273,
    "preview": "// Copyright 2022 Ainsley Clark. All rights reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "examples/attachments.go",
    "chars": 1444,
    "preview": "// Licensed under the Apache License, Version 2.0 (the \"License\");\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.

Copied to clipboard!