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
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
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:
go get github.com/go-resty/resty/v2package 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)
}