From 9db82f92c5fd0f756a27c39442578cf5df8f0ffc Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Tue, 18 Nov 2025 18:36:48 +0100 Subject: [PATCH] Add comprehensive documentation comments --- .gitignore | 6 +++++- client.go | 35 ++++++++++++++++++++++++++++++++-- errors.go | 8 +++++++- message.go | 54 +++++++++++++++++++++++++++++++++++++---------------- options.go | 29 ++++++++++++++++++++++++++++ response.go | 22 ++++++++++++++++------ 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 0a95508..d5bf228 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .idea/ -.vscode/ \ No newline at end of file +.vscode/ + +CLAUDE.md +.claude/ +.claude diff --git a/client.go b/client.go index e56babb..2e549ff 100644 --- a/client.go +++ b/client.go @@ -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) diff --git a/errors.go b/errors.go index 9d3e22a..2a6c1a1 100644 --- a/errors.go +++ b/errors.go @@ -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 diff --git a/message.go b/message.go index 279a877..2798269 100644 --- a/message.go +++ b/message.go @@ -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") diff --git a/options.go b/options.go index 322aad9..456655f 100644 --- a/options.go +++ b/options.go @@ -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 diff --git a/response.go b/response.go index 40af7d8..477f001 100644 --- a/response.go +++ b/response.go @@ -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 }