Tutorial
E-Mails mit Go senden
Senden Sie E-Mails aus Go mit dem SDK emailit-go oder net/smtp, behandeln Sie API-Fehler und verifizieren Sie Emailit-Webhook-Signaturen in einem net/http-Handler.
Diese Anleitung zeigt, wie Sie E-Mails aus einem Go-Dienst mit dem offiziellen SDK emailit-go senden, wie Sie stattdessen net/smtp verwenden und wie Sie Webhooks in einem net/http-Handler verifizieren.
Voraussetzungen
- Go 1.21 oder neuer.
- Eine verifizierte Versanddomain, zum Beispiel
acme.com. - Ein API-Schlüssel. Ein reiner Sende-Schlüssel, der auf Ihre Domain beschränkt ist, genügt.
- Solange Ihr Workspace keinen Produktionszugang hat, können Sie nur an die Konto-E-Mail-Adressen von Workspace-Mitgliedern senden.
SDK installieren
go get github.com/emailit/emailit-go/v2Das SDK hängt nur von der Go-Standardbibliothek ab.
API-Schlüssel konfigurieren
Übergeben Sie den Schlüssel als Umgebungsvariable an Ihren Prozess:
export EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••E-Mail senden
package main
import (
"log"
"os"
"github.com/emailit/emailit-go/v2"
)
func main() {
client := emailit.NewClient(os.Getenv("EMAILIT_API_KEY"))
email, err := client.Emails.Send(&emailit.SendEmailRequest{
From: "Acme <hello@acme.com>",
To: []string{"ada@example.com"},
Subject: "Your login code",
Text: "Your code is 482913. It expires in 10 minutes.",
Html: "<p>Your code is <strong>482913</strong>. It expires in 10 minutes.</p>",
})
if err != nil {
log.Fatal(err)
}
log.Println("sent", email.Id) // em_…
}Erstellen Sie den Client einmal und verwenden Sie ihn gemeinsam; er lässt sich gefahrlos über Anfragen hinweg wiederverwenden. Um eine gespeicherte Vorlage zu senden, setzen Sie Template auf ihren Alias oder ihre tem_-ID und übergeben Variables. Um Timeouts zu ändern, übergeben Sie Ihren eigenen Client: emailit.NewClient(key, emailit.WithHTTPClient(&http.Client{Timeout: 10 * time.Second})).
Fehler behandeln
Das SDK gibt gewöhnliche Go-Fehler zurück, mit Hilfsfunktionen, um sie einzuordnen:
email, err := client.Emails.Send(req)
if err != nil {
switch {
case emailit.IsRateLimitError(err):
// 429: back off and retry
case emailit.IsAuthenticationError(err):
// 401: the API key is missing or invalid
case emailit.IsUnprocessableEntityError(err):
// 422: for example, the from domain isn't verified
}
var apiErr *emailit.APIError
if errors.As(err, &apiErr) {
log.Printf("emailit: %d %s", apiErr.StatusCode, apiErr.Message)
}
return err
}Alternativ per SMTP senden
net/smtp aus der Standardbibliothek kann über das Emailit-Relay senden. smtp.SendMail stuft die Verbindung per STARTTLS hoch, bevor es sich authentifiziert:
package main
import (
"log"
"net/smtp"
"os"
"strings"
)
func main() {
auth := smtp.PlainAuth("", "emailit", os.Getenv("EMAILIT_API_KEY"), "smtp.emailit.com")
msg := strings.Join([]string{
"From: Acme <hello@acme.com>",
"To: ada@example.com",
"Subject: Your login code",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"",
"Your code is 482913. It expires in 10 minutes.",
}, "\r\n")
err := smtp.SendMail("smtp.emailit.com:587", auth, "hello@acme.com", []string{"ada@example.com"}, []byte(msg))
if err != nil {
log.Fatal(err)
}
}net/smtp ist minimal: Sie bauen die Nachricht selbst, und implizites TLS auf Port 465 gibt es nicht. Für HTML, Anhänge oder die Wiederverwendung von Verbindungen verwenden Sie eine gepflegte Mail-Bibliothek oder die API. Wenn Port 587 blockiert ist, verwenden Sie smtp.emailit.com:2525. Siehe SMTP-Einstellungen.
Webhooks empfangen
Erstellen Sie einen Webhook und speichern Sie sein Signatur-Secret in EMAILIT_WEBHOOK_SECRET. Der Handler liest den unveränderten Body, prüft die Signatur und dekodiert dann das Array der Events:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"strconv"
"time"
)
type emailitEvent struct {
EventID string `json:"event_id"`
Type string `json:"type"`
Data struct {
Object map[string]any `json:"object"`
} `json:"data"`
}
func validSignature(body []byte, signature, timestamp, secret string) bool {
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || signature == "" {
return false
}
if age := time.Now().Unix() - ts; age > 300 || age < -300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func emailitWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 10<<20))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !validSignature(body, r.Header.Get("X-Emailit-Signature"), r.Header.Get("X-Emailit-Timestamp"), os.Getenv("EMAILIT_WEBHOOK_SECRET")) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var events []emailitEvent // up to 100 events per request
if err := json.Unmarshal(body, &events); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
for _, event := range events {
if event.Type == "email.bounced" {
// Stop emailing event.Data.Object["to"].
}
}
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("POST /webhooks/emailit", emailitWebhook)
http.ListenAndServe(":8080", nil)
}Das Muster POST /path erfordert Go 1.22 oder neuer; unter Go 1.21 registrieren Sie /webhooks/emailit und prüfen r.Method selbst. Geben Sie innerhalb von 30 Sekunden einen 2xx-Status zurück; bei anderen Antworten wird die Anfrage wiederholt. Siehe Anfragesignatur.
Tipps für den Produktivbetrieb
- Unter dem Rate Limit bleiben. Neue Workspaces können standardmäßig 2 E-Mails pro Sekunde und 5.000 pro Tag senden. Senden Sie Massen-E-Mails aus einem Worker mit einem Rate Limiter wie
golang.org/x/time/rate. Siehe Limits. - Wiederholungen sicher machen. Wenn Sie nach einem Timeout erneut senden, übergeben Sie mit
net/httpeinen HeaderIdempotency-Key, damit die E-Mail nicht doppelt gesendet wird. Siehe Idempotenz. - Webhooks deduplizieren, indem Sie jede verarbeitete
EventIDspeichern. - Timeouts setzen. Verwenden Sie einen Context oder ein Timeout für den HTTP-Client, damit ein langsames Netzwerk keine Request-Goroutinen blockiert.