Initial release
This commit is contained in:
commit
70d1d6bce8
9 changed files with 576 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oliver Jakoubek, Beautiful Machines
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
225
README.md
Normal file
225
README.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# Sendamatic Go Client
|
||||
|
||||
A Go client library for the [Sendamatic](https://www.sendamatic.net) email delivery API.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple and idiomatic Go API
|
||||
- Context support for timeouts and cancellation
|
||||
- Fluent message builder interface
|
||||
- Support for HTML and plain text emails
|
||||
- File attachments with automatic base64 encoding
|
||||
- Custom headers
|
||||
- Multiple recipients (To, CC, BCC)
|
||||
- Comprehensive error handling
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
go get code.beautifulmachines.dev/jakoubek/sendamatic
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"code.beautifulmachines.dev/jakoubek/sendamatic"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client
|
||||
client := sendamatic.NewClient("your-user-id", "your-password")
|
||||
|
||||
// Build message
|
||||
msg := sendamatic.NewMessage().
|
||||
SetSender("sender@example.com").
|
||||
AddTo("recipient@example.com").
|
||||
SetSubject("Hello from Sendamatic").
|
||||
SetTextBody("This is a test message.").
|
||||
SetHTMLBody("<h1>Hello!</h1><p>This is a test message.</p>")
|
||||
|
||||
// Send email
|
||||
resp, err := client.Send(context.Background(), msg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Email sent successfully: %d", resp.StatusCode)
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Email
|
||||
```go
|
||||
msg := sendamatic.NewMessage().
|
||||
SetSender("sender@example.com").
|
||||
AddTo("recipient@example.com").
|
||||
SetSubject("Hello World").
|
||||
SetTextBody("This is a plain text email.")
|
||||
|
||||
resp, err := client.Send(context.Background(), msg)
|
||||
```
|
||||
|
||||
### HTML Email with Multiple Recipients
|
||||
```go
|
||||
msg := sendamatic.NewMessage().
|
||||
SetSender("newsletter@example.com").
|
||||
AddTo("user1@example.com").
|
||||
AddTo("user2@example.com").
|
||||
AddCC("manager@example.com").
|
||||
AddBCC("archive@example.com").
|
||||
SetSubject("Monthly Newsletter").
|
||||
SetHTMLBody("<h1>Newsletter</h1><p>Your monthly update...</p>").
|
||||
SetTextBody("Newsletter - Your monthly update...")
|
||||
```
|
||||
|
||||
### Email with Attachments
|
||||
```go
|
||||
// From file path
|
||||
msg := sendamatic.NewMessage().
|
||||
SetSender("sender@example.com").
|
||||
AddTo("recipient@example.com").
|
||||
SetSubject("Invoice").
|
||||
SetTextBody("Please find your invoice attached.")
|
||||
|
||||
err := msg.AttachFileFromPath("./invoice.pdf", "application/pdf")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Or from byte slice
|
||||
pdfData := []byte{...}
|
||||
msg.AttachFile("invoice.pdf", "application/pdf", pdfData)
|
||||
```
|
||||
|
||||
### Custom Headers
|
||||
```go
|
||||
msg := sendamatic.NewMessage().
|
||||
SetSender("sender@example.com").
|
||||
AddTo("recipient@example.com").
|
||||
SetSubject("Custom Headers").
|
||||
SetTextBody("Email with custom headers").
|
||||
AddHeader("Reply-To", "support@example.com").
|
||||
AddHeader("X-Priority", "1")
|
||||
```
|
||||
|
||||
### With Timeout
|
||||
```go
|
||||
client := sendamatic.NewClient(
|
||||
"user-id",
|
||||
"password",
|
||||
sendamatic.WithTimeout(45*time.Second),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Send(ctx, msg)
|
||||
```
|
||||
|
||||
### Custom HTTP Client
|
||||
```go
|
||||
httpClient := &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
},
|
||||
}
|
||||
|
||||
client := sendamatic.NewClient(
|
||||
"user-id",
|
||||
"password",
|
||||
sendamatic.WithHTTPClient(httpClient),
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The client supports various configuration options via the functional options pattern:
|
||||
```go
|
||||
client := sendamatic.NewClient(
|
||||
"user-id",
|
||||
"password",
|
||||
sendamatic.WithBaseURL("https://custom.api.url"),
|
||||
sendamatic.WithTimeout(60*time.Second),
|
||||
sendamatic.WithHTTPClient(customHTTPClient),
|
||||
)
|
||||
```
|
||||
|
||||
## Response Handling
|
||||
|
||||
The `SendResponse` provides methods to check the delivery status:
|
||||
```go
|
||||
resp, err := client.Send(ctx, msg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Check overall success
|
||||
if resp.IsSuccess() {
|
||||
log.Println("Email sent successfully")
|
||||
}
|
||||
|
||||
// Check individual recipient status
|
||||
for email := range resp.Recipients {
|
||||
if status, ok := resp.GetStatus(email); ok {
|
||||
log.Printf("Recipient %s: status %d", email, status)
|
||||
}
|
||||
if msgID, ok := resp.GetMessageID(email); ok {
|
||||
log.Printf("Message ID: %s", msgID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library provides typed errors for better error handling:
|
||||
```go
|
||||
resp, err := client.Send(ctx, msg)
|
||||
if err != nil {
|
||||
var apiErr *sendamatic.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
log.Printf("API error (status %d): %s", apiErr.StatusCode, apiErr.Message)
|
||||
if apiErr.ValidationErrors != "" {
|
||||
log.Printf("Validation: %s", apiErr.ValidationErrors)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Other error: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go 1.22 or higher
|
||||
- Valid Sendamatic account with API credentials
|
||||
|
||||
## API Credentials
|
||||
|
||||
Your API credentials consist of:
|
||||
- **User ID**: Your Mail Credential User ID
|
||||
- **Password**: Your Mail Credential Password
|
||||
|
||||
Find these in your Sendamatic dashboard under Mail Credentials.
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed API documentation, visit:
|
||||
- [Sendamatic API Documentation](https://docs.sendamatic.net/api/send/)
|
||||
- [Sendamatic Website](https://www.sendamatic.net)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
## Author
|
||||
|
||||
Oliver Jakoubek ([info@jakoubek.net](mailto:info@jakoubek.net))
|
||||
83
client.go
Normal file
83
client.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package sendamatic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://send.api.sendamatic.net"
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(userID, password string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
apiKey: fmt.Sprintf("%s-%s", userID, password),
|
||||
baseURL: defaultBaseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
// Optionen anwenden
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Send versendet eine E-Mail über die Sendamatic API
|
||||
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)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/send", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-api-key", c.apiKey)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// Fehlerbehandlung für 4xx und 5xx
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, parseErrorResponse(resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var sendResp SendResponse
|
||||
if err := json.Unmarshal(body, &sendResp.Recipients); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
sendResp.StatusCode = resp.StatusCode
|
||||
return &sendResp, nil
|
||||
}
|
||||
36
errors.go
Normal file
36
errors.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package sendamatic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// APIError repräsentiert einen API-Fehler
|
||||
type APIError struct {
|
||||
StatusCode int `json:"-"`
|
||||
Message string `json:"error"`
|
||||
ValidationErrors string `json:"validation_errors,omitempty"`
|
||||
JSONPath string `json:"json_path,omitempty"`
|
||||
Sender string `json:"sender,omitempty"`
|
||||
SMTPCode int `json:"smtp_code,omitempty"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e.ValidationErrors != "" {
|
||||
return fmt.Sprintf("sendamatic api error (status %d): %s (path: %s)",
|
||||
e.StatusCode, e.ValidationErrors, e.JSONPath)
|
||||
}
|
||||
return fmt.Sprintf("sendamatic api error (status %d): %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
func parseErrorResponse(statusCode int, body []byte) error {
|
||||
var apiErr APIError
|
||||
apiErr.StatusCode = statusCode
|
||||
|
||||
if err := json.Unmarshal(body, &apiErr); err != nil {
|
||||
// Fallback, falls JSON nicht parsebar ist
|
||||
apiErr.Message = string(body)
|
||||
}
|
||||
|
||||
return &apiErr
|
||||
}
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module code.beautifulmachines.dev/jakoubek/sendamatic
|
||||
|
||||
go 1.25.4
|
||||
147
message.go
Normal file
147
message.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package sendamatic
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Message repräsentiert eine E-Mail-Nachricht
|
||||
type Message struct {
|
||||
To []string `json:"to"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
BCC []string `json:"bcc,omitempty"`
|
||||
Sender string `json:"sender"`
|
||||
Subject string `json:"subject"`
|
||||
TextBody string `json:"text_body,omitempty"`
|
||||
HTMLBody string `json:"html_body,omitempty"`
|
||||
Headers []Header `json:"headers,omitempty"`
|
||||
Attachments []Attachment `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
// Header repräsentiert einen benutzerdefinierten E-Mail-Header
|
||||
type Header struct {
|
||||
Header string `json:"header"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Attachment repräsentiert einen E-Mail-Anhang
|
||||
type Attachment struct {
|
||||
Filename string `json:"filename"`
|
||||
Data string `json:"data"` // Base64-kodiert
|
||||
MimeType string `json:"mimetype"`
|
||||
}
|
||||
|
||||
// NewMessage erstellt eine neue Message
|
||||
func NewMessage() *Message {
|
||||
return &Message{
|
||||
To: []string{},
|
||||
CC: []string{},
|
||||
BCC: []string{},
|
||||
Headers: []Header{},
|
||||
Attachments: []Attachment{},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTo fügt einen Empfänger hinzu
|
||||
func (m *Message) AddTo(email string) *Message {
|
||||
m.To = append(m.To, email)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddCC fügt einen CC-Empfänger hinzu
|
||||
func (m *Message) AddCC(email string) *Message {
|
||||
m.CC = append(m.CC, email)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddBCC fügt einen BCC-Empfänger hinzu
|
||||
func (m *Message) AddBCC(email string) *Message {
|
||||
m.BCC = append(m.BCC, email)
|
||||
return m
|
||||
}
|
||||
|
||||
// SetSender setzt den Absender
|
||||
func (m *Message) SetSender(email string) *Message {
|
||||
m.Sender = email
|
||||
return m
|
||||
}
|
||||
|
||||
// SetSubject setzt den Betreff
|
||||
func (m *Message) SetSubject(subject string) *Message {
|
||||
m.Subject = subject
|
||||
return m
|
||||
}
|
||||
|
||||
// SetTextBody setzt den Text-Körper
|
||||
func (m *Message) SetTextBody(body string) *Message {
|
||||
m.TextBody = body
|
||||
return m
|
||||
}
|
||||
|
||||
// SetHTMLBody setzt den HTML-Körper
|
||||
func (m *Message) SetHTMLBody(body string) *Message {
|
||||
m.HTMLBody = body
|
||||
return m
|
||||
}
|
||||
|
||||
// AddHeader fügt einen benutzerdefinierten Header hinzu
|
||||
func (m *Message) AddHeader(name, value string) *Message {
|
||||
m.Headers = append(m.Headers, Header{
|
||||
Header: name,
|
||||
Value: value,
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// AttachFile fügt eine Datei als Anhang hinzu
|
||||
func (m *Message) AttachFile(filename, mimeType string, data []byte) *Message {
|
||||
m.Attachments = append(m.Attachments, Attachment{
|
||||
Filename: filename,
|
||||
Data: base64.StdEncoding.EncodeToString(data),
|
||||
MimeType: mimeType,
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// AttachFileFromPath lädt eine Datei vom Dateisystem und fügt sie als Anhang hinzu
|
||||
func (m *Message) AttachFileFromPath(path, mimeType string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extrahiere Dateinamen aus Pfad
|
||||
filename := path
|
||||
if idx := len(path) - 1; idx >= 0 {
|
||||
for i := idx; i >= 0; i-- {
|
||||
if path[i] == '/' || path[i] == '\\' {
|
||||
filename = path[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.AttachFile(filename, mimeType, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate prüft, ob die Message gültig ist
|
||||
func (m *Message) Validate() error {
|
||||
if len(m.To) == 0 {
|
||||
return errors.New("at least one recipient required")
|
||||
}
|
||||
if len(m.To) > 255 {
|
||||
return errors.New("maximum 255 recipients allowed")
|
||||
}
|
||||
if m.Sender == "" {
|
||||
return errors.New("sender is required")
|
||||
}
|
||||
if m.Subject == "" {
|
||||
return errors.New("subject is required")
|
||||
}
|
||||
if m.TextBody == "" && m.HTMLBody == "" {
|
||||
return errors.New("either text_body or html_body is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
26
options.go
Normal file
26
options.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package sendamatic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Option func(*Client)
|
||||
|
||||
func WithBaseURL(baseURL string) Option {
|
||||
return func(c *Client) {
|
||||
c.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
func WithHTTPClient(client *http.Client) Option {
|
||||
return func(c *Client) {
|
||||
c.httpClient = client
|
||||
}
|
||||
}
|
||||
|
||||
func WithTimeout(timeout time.Duration) Option {
|
||||
return func(c *Client) {
|
||||
c.httpClient.Timeout = timeout
|
||||
}
|
||||
}
|
||||
33
response.go
Normal file
33
response.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package sendamatic
|
||||
|
||||
// SendResponse repräsentiert die Antwort auf einen Send-Request
|
||||
type SendResponse struct {
|
||||
StatusCode int
|
||||
Recipients map[string][2]interface{} // Email -> [StatusCode, MessageID]
|
||||
}
|
||||
|
||||
// IsSuccess prüft, ob die gesamte Sendung erfolgreich war
|
||||
func (r *SendResponse) IsSuccess() bool {
|
||||
return r.StatusCode == 200
|
||||
}
|
||||
|
||||
// GetMessageID gibt die Message-ID für einen Empfänger zurück
|
||||
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 {
|
||||
return msgID, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetStatus gibt den Status-Code für einen Empfänger zurück
|
||||
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
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue