# Go

net/http, no dependencies, with the refusal decoded into an error you can match on.

There is no emails.sh Go module to add. net/http and encoding/json cover the whole API, and the program below compiles as it stands.

### Send

email/email.go:
```go
// email/email.go
package email

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type Send struct {
	From           string            `json:"from"`
	To             []string          `json:"to"`
	Subject        string            `json:"subject"`
	HTML           string            `json:"html,omitempty"`
	Text           string            `json:"text,omitempty"`
	Tags           map[string]string `json:"tags,omitempty"`
	IdempotencyKey string            `json:"idempotency_key,omitempty"`
}

type Result struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

// Refusal is what the API returns instead of an id: {"error": {"code",
// "message", "next"}}. Code is machine readable, Next says what to do about it.
type Refusal struct {
	Body struct {
		Code    string `json:"code"`
		Message string `json:"message"`
		Next    string `json:"next"`
	} `json:"error"`
}

func (r *Refusal) Error() string {
	return r.Body.Code + ": " + r.Body.Message + " " + r.Body.Next
}

var client = &http.Client{Timeout: 10 * time.Second}

// SendEmail posts one email. The key comes from
// https://emails.sh/dashboard/api-keys via EMAILSSH_API_KEY.
func SendEmail(ctx context.Context, send Send) (Result, error) {
	body, err := json.Marshal(send)
	if err != nil {
		return Result{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://emails.sh/v1/emails", bytes.NewReader(body))
	if err != nil {
		return Result{}, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EMAILSSH_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := client.Do(req)
	if err != nil {
		return Result{}, fmt.Errorf("emails.sh unreachable: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		refusal := &Refusal{}
		if err := json.NewDecoder(res.Body).Decode(refusal); err != nil {
			return Result{}, fmt.Errorf("emails.sh returned %d", res.StatusCode)
		}
		return Result{}, refusal
	}

	result := Result{}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		return Result{}, err
	}
	return result, nil
}
```

main.go:
```go
// main.go
package main

import (
	"context"
	"errors"
	"log"

	"example.com/app/email"
)

func main() {
	result, err := email.SendEmail(context.Background(), email.Send{
		From:    "Acme <hello@acme.com>",
		To:      []string{"ada@example.com"},
		Subject: "Your receipt from Acme",
		HTML:    "<p>Thanks for your order.</p>",
	})

	refusal := &email.Refusal{}
	switch {
	case errors.As(err, &refusal):
		log.Fatalf("refused: %s", refusal)
	case err != nil:
		log.Fatal(err)
	}

	log.Printf("queued %s (%s)", result.ID, result.Status)
}
```

### If you would rather use a client library

Any HTTP client works, since there is nothing to negotiate beyond a bearer token. resty, for example:

Optional:
```bash
go get github.com/go-resty/resty/v2
```

main.go with resty:
```go
package main

import (
	"log"
	"os"

	"github.com/go-resty/resty/v2"
)

func main() {
	var result struct {
		ID     string `json:"id"`
		Status string `json:"status"`
	}

	res, err := resty.New().R().
		SetAuthToken(os.Getenv("EMAILSSH_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(map[string]any{
			"from":    "Acme <hello@acme.com>",
			"to":      []string{"ada@example.com"},
			"subject": "Your receipt from Acme",
			"html":    "<p>Thanks for your order.</p>",
		}).
		SetResult(&result).
		Post("https://emails.sh/v1/emails")

	if err != nil {
		log.Fatal(err)
	}
	if res.IsError() {
		log.Fatalf("refused: %s", res.String())
	}
	log.Printf("queued %s", result.ID)
}
```

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
