Add comprehensive documentation comments

This commit is contained in:
Oliver Jakoubek 2025-11-18 18:36:48 +01:00
commit 9db82f92c5
6 changed files with 128 additions and 26 deletions

6
.gitignore vendored
View file

@ -1,2 +1,6 @@
.idea/
.vscode/
.vscode/
CLAUDE.md
.claude/
.claude

View file

@ -1,3 +1,17 @@
// Package sendamatic provides a Go client library for the Sendamatic email delivery API.
//
// The library offers a simple and idiomatic Go API with context support, a fluent message
// builder interface, and comprehensive error handling for sending transactional emails.
//
// Example usage:
//
// client := sendamatic.NewClient("your-user-id", "your-password")
// msg := sendamatic.NewMessage().
// SetSender("sender@example.com").
// AddTo("recipient@example.com").
// SetSubject("Hello").
// SetTextBody("Hello World")
// resp, err := client.Send(context.Background(), msg)
package sendamatic
import (
@ -11,16 +25,28 @@ import (
)
const (
// defaultBaseURL is the default Sendamatic API endpoint.
defaultBaseURL = "https://send.api.sendamatic.net"
// defaultTimeout is the default HTTP client timeout for API requests.
defaultTimeout = 30 * time.Second
)
// Client represents a Sendamatic API client that handles authentication and HTTP communication
// with the Sendamatic email delivery service.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
// NewClient creates and returns a new Client configured with the provided Sendamatic credentials.
// The userID and password are combined to form the API key used for authentication.
// Optional configuration functions can be provided to customize the client behavior.
//
// Example:
//
// client := sendamatic.NewClient("user-id", "password",
// sendamatic.WithTimeout(60*time.Second))
func NewClient(userID, password string, opts ...Option) *Client {
c := &Client{
apiKey: fmt.Sprintf("%s-%s", userID, password),
@ -30,7 +56,7 @@ func NewClient(userID, password string, opts ...Option) *Client {
},
}
// Optionen anwenden
// Apply configuration options
for _, opt := range opts {
opt(c)
}
@ -38,7 +64,12 @@ func NewClient(userID, password string, opts ...Option) *Client {
return c
}
// Send versendet eine E-Mail über die Sendamatic API
// Send sends an email message through the Sendamatic API using the provided context.
// The message is validated before sending. If validation fails or the API request fails,
// an error is returned. On success, a SendResponse containing per-recipient delivery
// information is returned.
//
// The context can be used to set deadlines, timeouts, or cancel the request.
func (c *Client) Send(ctx context.Context, msg *Message) (*SendResponse, error) {
if err := msg.Validate(); err != nil {
return nil, fmt.Errorf("message validation failed: %w", err)

View file

@ -5,7 +5,9 @@ import (
"fmt"
)
// APIError repräsentiert einen API-Fehler
// APIError represents an error response from the Sendamatic API.
// It includes the HTTP status code, error message, and optional additional context
// such as validation errors, JSON path information, and SMTP codes.
type APIError struct {
StatusCode int `json:"-"`
Message string `json:"error"`
@ -15,6 +17,8 @@ type APIError struct {
SMTPCode int `json:"smtp_code,omitempty"`
}
// Error implements the error interface and returns a formatted error message.
// If validation errors are present, they are included with the JSON path context.
func (e *APIError) Error() string {
if e.ValidationErrors != "" {
return fmt.Sprintf("sendamatic api error (status %d): %s (path: %s)",
@ -23,6 +27,8 @@ func (e *APIError) Error() string {
return fmt.Sprintf("sendamatic api error (status %d): %s", e.StatusCode, e.Message)
}
// parseErrorResponse attempts to parse an API error response body into an APIError.
// If the body cannot be parsed as JSON, it uses the raw body as the error message.
func parseErrorResponse(statusCode int, body []byte) error {
var apiErr APIError
apiErr.StatusCode = statusCode

View file

@ -6,7 +6,9 @@ import (
"os"
)
// Message repräsentiert eine E-Mail-Nachricht
// Message represents an email message with all its components including recipients,
// content, headers, and attachments. Messages are constructed using the fluent builder
// pattern provided by the setter methods.
type Message struct {
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
@ -19,20 +21,21 @@ type Message struct {
Attachments []Attachment `json:"attachments,omitempty"`
}
// Header repräsentiert einen benutzerdefinierten E-Mail-Header
// Header represents a custom email header as a name-value pair.
type Header struct {
Header string `json:"header"`
Value string `json:"value"`
}
// Attachment repräsentiert einen E-Mail-Anhang
// Attachment represents an email attachment with its filename, MIME type, and base64-encoded data.
type Attachment struct {
Filename string `json:"filename"`
Data string `json:"data"` // Base64-kodiert
Data string `json:"data"` // Base64-encoded file content
MimeType string `json:"mimetype"`
}
// NewMessage erstellt eine neue Message
// NewMessage creates and returns a new empty Message with initialized slices for recipients,
// headers, and attachments. Use the setter methods to populate the message fields.
func NewMessage() *Message {
return &Message{
To: []string{},
@ -43,49 +46,58 @@ func NewMessage() *Message {
}
}
// AddTo fügt einen Empfänger hinzu
// AddTo adds a recipient email address to the To field.
// Returns the message for method chaining.
func (m *Message) AddTo(email string) *Message {
m.To = append(m.To, email)
return m
}
// AddCC fügt einen CC-Empfänger hinzu
// AddCC adds a recipient email address to the CC (carbon copy) field.
// Returns the message for method chaining.
func (m *Message) AddCC(email string) *Message {
m.CC = append(m.CC, email)
return m
}
// AddBCC fügt einen BCC-Empfänger hinzu
// AddBCC adds a recipient email address to the BCC (blind carbon copy) field.
// Returns the message for method chaining.
func (m *Message) AddBCC(email string) *Message {
m.BCC = append(m.BCC, email)
return m
}
// SetSender setzt den Absender
// SetSender sets the sender email address for the message.
// Returns the message for method chaining.
func (m *Message) SetSender(email string) *Message {
m.Sender = email
return m
}
// SetSubject setzt den Betreff
// SetSubject sets the email subject line.
// Returns the message for method chaining.
func (m *Message) SetSubject(subject string) *Message {
m.Subject = subject
return m
}
// SetTextBody setzt den Text-Körper
// SetTextBody sets the plain text body of the email.
// Returns the message for method chaining.
func (m *Message) SetTextBody(body string) *Message {
m.TextBody = body
return m
}
// SetHTMLBody setzt den HTML-Körper
// SetHTMLBody sets the HTML body of the email.
// Returns the message for method chaining.
func (m *Message) SetHTMLBody(body string) *Message {
m.HTMLBody = body
return m
}
// AddHeader fügt einen benutzerdefinierten Header hinzu
// AddHeader adds a custom email header with the specified name and value.
// Common examples include "Reply-To", "X-Priority", or custom application headers.
// Returns the message for method chaining.
func (m *Message) AddHeader(name, value string) *Message {
m.Headers = append(m.Headers, Header{
Header: name,
@ -94,7 +106,9 @@ func (m *Message) AddHeader(name, value string) *Message {
return m
}
// AttachFile fügt eine Datei als Anhang hinzu
// AttachFile adds a file attachment to the message from a byte slice.
// The data is automatically base64-encoded for transmission.
// Returns the message for method chaining.
func (m *Message) AttachFile(filename, mimeType string, data []byte) *Message {
m.Attachments = append(m.Attachments, Attachment{
Filename: filename,
@ -104,7 +118,9 @@ func (m *Message) AttachFile(filename, mimeType string, data []byte) *Message {
return m
}
// AttachFileFromPath lädt eine Datei vom Dateisystem und fügt sie als Anhang hinzu
// AttachFileFromPath reads a file from the filesystem and adds it as an attachment.
// The filename is extracted from the path. Returns an error if the file cannot be read.
// The file data is automatically base64-encoded for transmission.
func (m *Message) AttachFileFromPath(path, mimeType string) error {
data, err := os.ReadFile(path)
if err != nil {
@ -126,7 +142,13 @@ func (m *Message) AttachFileFromPath(path, mimeType string) error {
return nil
}
// Validate prüft, ob die Message gültig ist
// Validate checks whether the message meets all required criteria for sending.
// It returns an error if any validation rules are violated:
// - At least one recipient is required
// - Maximum of 255 recipients allowed
// - Sender must be specified
// - Subject must be specified
// - Either TextBody or HTMLBody (or both) must be provided
func (m *Message) Validate() error {
if len(m.To) == 0 {
return errors.New("at least one recipient required")

View file

@ -5,20 +5,49 @@ import (
"time"
)
// Option is a function type that modifies a Client during initialization.
// Options follow the functional options pattern for configuring client behavior.
type Option func(*Client)
// WithBaseURL returns an Option that sets a custom API base URL for the client.
// Use this to point to a different Sendamatic API endpoint or a testing environment.
//
// Example:
//
// client := sendamatic.NewClient("user", "pass",
// sendamatic.WithBaseURL("https://custom.api.url"))
func WithBaseURL(baseURL string) Option {
return func(c *Client) {
c.baseURL = baseURL
}
}
// WithHTTPClient returns an Option that replaces the default HTTP client with a custom one.
// This allows full control over HTTP behavior such as transport settings, connection pooling,
// and custom middleware.
//
// Example:
//
// customClient := &http.Client{
// Timeout: 60 * time.Second,
// Transport: customTransport,
// }
// client := sendamatic.NewClient("user", "pass",
// sendamatic.WithHTTPClient(customClient))
func WithHTTPClient(client *http.Client) Option {
return func(c *Client) {
c.httpClient = client
}
}
// WithTimeout returns an Option that sets the HTTP client timeout duration.
// This determines how long the client will wait for a response before timing out.
// The default timeout is 30 seconds.
//
// Example:
//
// client := sendamatic.NewClient("user", "pass",
// sendamatic.WithTimeout(60*time.Second))
func WithTimeout(timeout time.Duration) Option {
return func(c *Client) {
c.httpClient.Timeout = timeout

View file

@ -1,17 +1,23 @@
package sendamatic
// SendResponse repräsentiert die Antwort auf einen Send-Request
// SendResponse represents the response from a send email request.
// It contains the overall HTTP status code and per-recipient delivery information
// including individual status codes and message IDs.
type SendResponse struct {
StatusCode int
Recipients map[string][2]interface{} // Email -> [StatusCode, MessageID]
Recipients map[string][2]interface{} // Email address -> [status code, message ID]
}
// IsSuccess prüft, ob die gesamte Sendung erfolgreich war
// IsSuccess returns true if the email send request was successful (HTTP 200).
// Note that this checks the overall request status; individual recipients
// may still have failed. Use GetStatus to check per-recipient delivery status.
func (r *SendResponse) IsSuccess() bool {
return r.StatusCode == 200
}
// GetMessageID gibt die Message-ID für einen Empfänger zurück
// GetMessageID returns the message ID for a specific recipient email address.
// The message ID can be used to track the email in logs or with the email provider.
// Returns the message ID and true if found, or empty string and false if not found.
func (r *SendResponse) GetMessageID(email string) (string, bool) {
if info, ok := r.Recipients[email]; ok && len(info) >= 2 {
if msgID, ok := info[1].(string); ok {
@ -21,10 +27,14 @@ func (r *SendResponse) GetMessageID(email string) (string, bool) {
return "", false
}
// GetStatus gibt den Status-Code für einen Empfänger zurück
// GetStatus returns the delivery status code for a specific recipient email address.
// The status code indicates whether the email was accepted for delivery to that recipient.
// Returns the status code and true if found, or 0 and false if not found.
//
// Note: The API returns status codes as JSON numbers which are decoded as float64,
// so this method performs the necessary type conversion to int.
func (r *SendResponse) GetStatus(email string) (int, bool) {
if info, ok := r.Recipients[email]; ok && len(info) >= 1 {
// Die API gibt float64 zurück, da JSON numbers als float64 dekodiert werden
if status, ok := info[0].(float64); ok {
return int(status), true
}