A lightweight and modular Go client for interacting with the WildDuck API.
This SDK mirrors the endpoint coverage of wildduck-nodesdk while using idiomatic Go patterns such as context.Context, net/http, and explicit error returns.
- Modular API surface for authentication, users, addresses, mailboxes, messages, submissions, filters, and autoreplies.
- WildDuck authentication through the
X-Access-Tokenheader. - Simple request payloads and query parameters with
wildduck.M. - Context-aware calls for deadlines, cancellation, and request scoping.
- Custom HTTP client support for timeouts, tracing, proxies, or tests.
- Raw byte helpers for message sources and attachments.
- No third-party runtime dependencies.
Install the module with go get:
go get github.com/tenforwardab/wildduck-gosdkImport it in your Go code:
import wildduck "github.com/tenforwardab/wildduck-gosdk"Create a client with your WildDuck API key and API base URL:
package main
import (
"context"
"fmt"
"log"
wildduck "github.com/tenforwardab/wildduck-gosdk"
)
func main() {
ctx := context.Background()
api := wildduck.New("YOUR_API_KEY", "https://api.example.com")
users, err := api.Users.List(ctx, wildduck.M{"limit": 20})
if err != nil {
log.Fatal(err)
}
fmt.Println(users["results"])
}Use WithHTTPClient when you need custom transport settings:
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
api := wildduck.New(
"YOUR_API_KEY",
"https://api.example.com",
wildduck.WithHTTPClient(httpClient),
)The root client exposes these services:
api.Authenticationapi.Usersapi.Addressesapi.Mailboxesapi.Messagesapi.Submissionapi.Filtersapi.Autoreplies
All methods take context.Context. JSON request bodies and query strings are passed as wildduck.M, which is an alias for map[string]any.
Check whether a username exists and can authenticate for a scope:
response, err := api.Authentication.PreAuth(ctx, "user@example.com", "imap", wildduck.M{
"sess": "session-id",
"ip": "192.0.2.10",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response["success"])Authenticate a user and optionally request a temporary access token:
auth, err := api.Authentication.Authenticate(ctx, "user@example.com", "password123", wildduck.M{
"scope": "imap",
"protocol": "imap",
"token": true,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(auth)Invalidate the current access token:
result, err := api.Authentication.InvalidateToken(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(result["success"])users, err := api.Users.List(ctx, wildduck.M{
"query": "example",
"limit": 50,
"metaData": true,
})created, err := api.Users.Create(ctx, wildduck.M{
"username": "alice",
"password": "securePassword123",
"name": "Alice Example",
"address": "alice@example.com",
"quota": 1073741824,
"tags": []string{"customer", "active"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(created["id"])resolved, err := api.Users.Resolve(ctx, "alice", nil)
userID := resolved["id"].(string)
user, err := api.Users.Get(ctx, userID, wildduck.M{"metaData": true})
updated, err := api.Users.Update(ctx, userID, wildduck.M{
"name": "Alice Admin",
"spamLevel": 80,
})
deleted, err := api.Users.Delete(ctx, userID, wildduck.M{
"deleteAfter": "2026-12-31T23:59:59Z",
})
_, _, _, _ = user, updated, deleted, err_, err = api.Users.Logout(ctx, userID, wildduck.M{
"reason": "Administrative logout",
})
quota, err := api.Users.RecalculateQuota(ctx, userID, nil)
task, err := api.Users.RecalculateAllQuota(ctx, nil)
password, err := api.Users.ResetPassword(ctx, userID, wildduck.M{
"validAfter": "2026-06-16T10:00:00Z",
})
recovery, err := api.Users.GetRecoveryInfo(ctx, userID, nil)
restored, err := api.Users.CancelDeletion(ctx, userID, nil)
_, _, _, _, _ = quota, task, password, recovery, restoredThe Go SDK returns the stream URL so the caller can choose an SSE implementation:
streamURL, err := api.Users.ChangeStreamURL(userID, wildduck.M{
"LastEventID": "previous-event-id",
})addresses, err := api.Addresses.List(ctx, wildduck.M{
"query": "example.com",
"limit": 20,
})
address, err := api.Addresses.Create(ctx, userID, wildduck.M{
"address": "alice@example.com",
"name": "Alice Example",
"main": true,
"tags": []string{"primary"},
})
_, _ = addresses, addressuserAddresses, err := api.Addresses.ListForUser(ctx, userID, wildduck.M{
"metaData": true,
})
addressInfo, err := api.Addresses.Get(ctx, userID, "ADDRESS_ID", nil)
_, err = api.Addresses.Update(ctx, userID, "ADDRESS_ID", wildduck.M{
"name": "Updated Identity",
"main": true,
})
_, err = api.Addresses.Delete(ctx, userID, "ADDRESS_ID", nil)
_, _ = userAddresses, addressInfoforwarded, err := api.Addresses.CreateForwarded(ctx, wildduck.M{
"address": "sales@example.com",
"name": "Sales",
"targets": []string{"sales-team@example.net"},
})
_, err = api.Addresses.UpdateForwarded(ctx, "ADDRESS_ID", wildduck.M{
"targets": []string{"new-target@example.net"},
})
info, err := api.Addresses.GetForwarded(ctx, "ADDRESS_ID", nil)
_, err = api.Addresses.DeleteForwarded(ctx, "ADDRESS_ID", nil)
_, _ = forwarded, inforegister, err := api.Addresses.ListRegister(ctx, userID, wildduck.M{
"query": "ali",
"limit": 25,
})
resolvedAddress, err := api.Addresses.Resolve(ctx, "alice@example.com", wildduck.M{
"allowWildcard": true,
})
renamed, err := api.Addresses.RenameDomain(ctx, wildduck.M{
"oldDomain": "old.example.com",
"newDomain": "new.example.com",
})
_, _, _ = register, resolvedAddress, renamedmailboxes, err := api.Mailboxes.List(ctx, userID, wildduck.M{
"counters": true,
"sizes": true,
})mailbox, err := api.Mailboxes.Create(ctx, userID, wildduck.M{
"path": "Projects/Go",
"retention": 0,
})
mailboxInfo, err := api.Mailboxes.Get(ctx, userID, "MAILBOX_ID", nil)
_, err = api.Mailboxes.Update(ctx, userID, "MAILBOX_ID", wildduck.M{
"path": "Projects/WildDuck",
"subscribed": true,
})
_, err = api.Mailboxes.Delete(ctx, userID, "MAILBOX_ID", nil)
_, _, _ = mailboxes, mailbox, mailboxInfomessages, err := api.Messages.List(ctx, userID, "MAILBOX_ID", wildduck.M{
"unseen": true,
"limit": 25,
"order": "desc",
})
search, err := api.Messages.Search(ctx, userID, wildduck.M{
"query": "invoice",
"searchable": true,
"limit": 10,
})
_, _ = messages, searchuploaded, err := api.Messages.Upload(ctx, userID, "MAILBOX_ID", wildduck.M{
"from": wildduck.M{"name": "Alice", "address": "alice@example.com"},
"to": []wildduck.M{
{"name": "Bob", "address": "bob@example.com"},
},
"subject": "Stored message",
"text": "This message is uploaded to a mailbox.",
})updated, err := api.Messages.UpdateMany(ctx, userID, "MAILBOX_ID", wildduck.M{
"message": "1,2,3",
"seen": true,
})
single, err := api.Messages.Update(ctx, userID, "MAILBOX_ID", "123", wildduck.M{
"flagged": true,
})
_, err = api.Messages.Delete(ctx, userID, "MAILBOX_ID", 123, nil)
_, err = api.Messages.DeleteMany(ctx, userID, "MAILBOX_ID", wildduck.M{
"async": true,
})
_, _, _ = uploaded, updated, singlemessageInfo, err := api.Messages.Get(ctx, userID, "MAILBOX_ID", 123, wildduck.M{
"markAsSeen": true,
})
source, err := api.Messages.Source(ctx, userID, "MAILBOX_ID", 123, nil)
attachment, err := api.Messages.Attachment(ctx, userID, "MAILBOX_ID", 123, "ATTACHMENT_ID", false)
_, _, _ = messageInfo, source, attachmentFor large message sources or attachments, use the streaming variants to avoid buffering the full response in memory:
sourceBody, err := api.Messages.SourceStream(ctx, userID, "MAILBOX_ID", 123, nil)
if err != nil {
log.Fatal(err)
}
defer sourceBody.Close()
attachmentBody, err := api.Messages.AttachmentStream(ctx, userID, "MAILBOX_ID", 123, "ATTACHMENT_ID", false)
if err != nil {
log.Fatal(err)
}
defer attachmentBody.Close()forwardedMessage, err := api.Messages.Forward(ctx, userID, "MAILBOX_ID", 123, wildduck.M{
"addresses": []string{"target@example.net"},
})
submittedDraft, err := api.Messages.SubmitDraft(ctx, userID, "MAILBOX_ID", 456, wildduck.M{
"deleteFiles": true,
})
_, err = api.Messages.DeleteOutbound(ctx, userID, "QUEUE_ID", nil)
_, _ = forwardedMessage, submittedDraftSend or upload a message through the WildDuck submission endpoint:
submitted, err := api.Submission.Submit(ctx, userID, wildduck.M{
"mailbox": "MAILBOX_ID",
"from": wildduck.M{"name": "Alice", "address": "alice@example.com"},
"to": []wildduck.M{
{"name": "Bob", "address": "bob@example.com"},
},
"subject": "Hello from WildDuck Go SDK",
"text": "This was sent through the WildDuck API.",
"headers": []wildduck.M{
{"key": "X-App", "value": "wildduck-gosdk"},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(submitted["message"])allFilters, err := api.Filters.ListAll(ctx, wildduck.M{
"metaData": true,
"limit": 50,
})
userFilters, err := api.Filters.ListForUser(ctx, userID, wildduck.M{
"metaData": true,
})
_, _ = allFilters, userFiltersfilter, err := api.Filters.Create(ctx, userID, wildduck.M{
"name": "Invoices",
"query": wildduck.M{
"subject": "Invoice",
},
"action": wildduck.M{
"seen": true,
"mailbox": "MAILBOX_ID",
},
})
filterInfo, err := api.Filters.Get(ctx, userID, "FILTER_ID", nil)
_, err = api.Filters.Update(ctx, userID, "FILTER_ID", wildduck.M{
"name": "Updated invoices",
"query": wildduck.M{"subject": "Invoice"},
"action": wildduck.M{"seen": true},
"disabled": false,
})
_, err = api.Filters.Delete(ctx, userID, "FILTER_ID", nil)
_, _ = filter, filterInfoautoreply, err := api.Autoreplies.Get(ctx, userID, nil)
updatedAutoreply, err := api.Autoreplies.Update(ctx, userID, wildduck.M{
"status": true,
"name": "Alice Example",
"subject": "Out of office",
"text": "I am currently out of office.",
"html": "<p>I am currently out of office.</p>",
"start": "2026-07-01T00:00:00Z",
"end": "2026-07-15T00:00:00Z",
})
_, err = api.Autoreplies.Delete(ctx, userID, nil)
_, _ = autoreply, updatedAutoreplyNetwork and JSON errors are returned directly. Non-2xx HTTP responses are returned as *wildduck.APIError and include the HTTP status code, raw response body, and parsed Code/Message fields when WildDuck returns a JSON error payload.
_, err := api.Users.List(ctx, nil)
if err != nil {
if apiErr, ok := err.(*wildduck.APIError); ok {
fmt.Printf("WildDuck returned HTTP %d: %s %s\n", apiErr.StatusCode, apiErr.Code, apiErr.Message)
return
}
log.Fatal(err)
}my-project/
├── cmd/
│ └── app/
│ └── main.go
├── internal/
│ └── mail/
│ └── wildduck.go
├── go.mod
└── go.sum
Example internal/mail/wildduck.go:
package mail
import wildduck "github.com/tenforwardab/wildduck-gosdk"
func NewWildDuck(apiKey, apiURL string) *wildduck.Client {
return wildduck.New(apiKey, apiURL)
}This SDK is intentionally thin. WildDuck response payloads can vary by endpoint and API version, so methods return wildduck.M for JSON responses. For stricter application code, unmarshal or map the returned values into your own domain structs at the application boundary.
Contributions are welcome. Please keep changes focused, add tests for client behavior where practical, and keep the SDK aligned with WildDuck API endpoints.
This project is licensed under the EUPL v1.2.