Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ Account & organization management:
- Account accesses — list & remove user/invite/token access — [`examples/account-accesses`](examples/account-accesses)
- Permissions — list resources & bulk-update access permissions — [`examples/permissions`](examples/permissions)

Contact management:

- Contacts — create, get, update (upsert) & delete by ID or email — [`examples/contacts`](examples/contacts)
- Contact events — record custom events for a contact — [`examples/contact-events`](examples/contact-events)

## Errors

Non-2xx responses decode into typed errors that work with `errors.As`:
Expand Down
7 changes: 7 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ type Client struct {
AccountAccesses *AccountAccessesService
// Permissions lists account resources and bulk-updates access permissions.
Permissions *PermissionsService

// Contacts manages email marketing contacts.
Contacts *ContactsService
// ContactEvents records custom events for contacts.
ContactEvents *ContactEventsService
}

// Ptr returns a pointer to v, for setting optional pointer request fields such
Expand Down Expand Up @@ -142,6 +147,8 @@ func NewClient(token string, opts ...Option) (*Client, error) {
c.Accounts = &AccountsService{client: c}
c.AccountAccesses = &AccountAccessesService{client: c}
c.Permissions = &PermissionsService{client: c}
c.Contacts = &ContactsService{client: c}
c.ContactEvents = &ContactEventsService{client: c}

return c, nil
}
Expand Down
35 changes: 35 additions & 0 deletions contact_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package mailtrap

import (
"context"
"net/http"
)

// ContactEventsService records custom events for a contact.
type ContactEventsService struct {
client *Client
}

// ContactEvent is a custom event recorded against a contact.
type ContactEvent struct {
ContactID string `json:"contact_id"`
ContactEmail string `json:"contact_email"`
Name string `json:"name"`
// Params maps event parameter names to scalar values.
Params map[string]any `json:"params,omitempty"`
}

// CreateContactEventRequest is the payload for recording an event. Name is
// required (max 255 characters).
type CreateContactEventRequest struct {
Name string `json:"name"`
Params map[string]any `json:"params,omitempty"`
}

// Create records an event for the contact identified by UUID or email.
func (s *ContactEventsService) Create(ctx context.Context, identifier string, req *CreateContactEventRequest) (*ContactEvent, *Response, error) {
path := contactPath(identifier) + "/events"
event := new(ContactEvent)
resp, err := s.client.do(ctx, HostGeneral, http.MethodPost, path, nil, req, event)
return event, resp, err
}
29 changes: 29 additions & 0 deletions contact_events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mailtrap_test

import (
"context"
"net/http"
"testing"

"github.com/mailtrap/mailtrap-go"
)

func TestContactEvents_Create(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("POST /api/contacts/018dd5e3/events", func(w http.ResponseWriter, r *http.Request) {
wantJSONBody(t, r, `{"name":"UserLogin","params":{"user_id":101,"is_active":true}}`)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"contact_id":"018dd5e3","contact_email":"john@example.com","name":"UserLogin","params":{"user_id":101}}`))
})

event, _, err := client.ContactEvents.Create(context.Background(), "018dd5e3", &mailtrap.CreateContactEventRequest{
Name: "UserLogin",
Params: map[string]any{"user_id": 101, "is_active": true},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if event.ContactEmail != "john@example.com" || event.Name != "UserLogin" {
t.Errorf("event = %+v", event)
}
}
98 changes: 98 additions & 0 deletions contacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package mailtrap

import (
"context"
"net/http"
"net/url"
)

// ContactsService manages email marketing contacts.
type ContactsService struct {
client *Client
}

// Contact subscription statuses for Contact.Status.
const (
ContactStatusSubscribed = "subscribed"
ContactStatusUnsubscribed = "unsubscribed"
)

// Contact is an email marketing contact.
type Contact struct {
// ID is the contact's UUID.
ID string `json:"id"`
Email string `json:"email"`
// Fields maps custom field merge tags to scalar values.
Fields map[string]any `json:"fields,omitempty"`
ListIDs []int64 `json:"list_ids,omitempty"`
// Status is the subscription status.
Status string `json:"status,omitempty"`
// CreatedAt and UpdatedAt are Unix timestamps in milliseconds.
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}

// CreateContactRequest is the payload for creating a contact. Email is required.
type CreateContactRequest struct {
Email string `json:"email"`
Fields map[string]any `json:"fields,omitempty"`
ListIDs []int64 `json:"list_ids,omitempty"`
}

// UpdateContactRequest is the payload for updating (upserting) a contact. Email
// is required; ListIDsIncluded and ListIDsExcluded add and remove list
// memberships.
type UpdateContactRequest struct {
Email string `json:"email"`
Fields map[string]any `json:"fields,omitempty"`
ListIDsIncluded []int64 `json:"list_ids_included,omitempty"`
ListIDsExcluded []int64 `json:"list_ids_excluded,omitempty"`
Unsubscribed *bool `json:"unsubscribed,omitempty"`
}

// ContactUpsert is the result of Update: the contact plus whether it was
// created or updated.
type ContactUpsert struct {
// Action is "created" or "updated".
Action string `json:"action"`
Contact *Contact `json:"data"`
}

// Create adds a contact.
func (s *ContactsService) Create(ctx context.Context, req *CreateContactRequest) (*Contact, *Response, error) {
var wrapper struct {
Data *Contact `json:"data"`
}
body := map[string]any{"contact": req}
resp, err := s.client.do(ctx, HostGeneral, http.MethodPost, "/api/contacts", nil, body, &wrapper)
return wrapper.Data, resp, err
}

// Get returns a contact by UUID or email.
func (s *ContactsService) Get(ctx context.Context, identifier string) (*Contact, *Response, error) {
var wrapper struct {
Data *Contact `json:"data"`
}
resp, err := s.client.do(ctx, HostGeneral, http.MethodGet, contactPath(identifier), nil, nil, &wrapper)
return wrapper.Data, resp, err
}

// Update creates or updates (upserts) the contact identified by UUID or email,
// reporting which action was taken.
func (s *ContactsService) Update(ctx context.Context, identifier string, req *UpdateContactRequest) (*ContactUpsert, *Response, error) {
upsert := new(ContactUpsert)
body := map[string]any{"contact": req}
resp, err := s.client.do(ctx, HostGeneral, http.MethodPatch, contactPath(identifier), nil, body, upsert)
return upsert, resp, err
}

// Delete removes a contact by UUID or email.
func (s *ContactsService) Delete(ctx context.Context, identifier string) (*Response, error) {
return s.client.do(ctx, HostGeneral, http.MethodDelete, contactPath(identifier), nil, nil, nil)
}

// contactPath builds the path for a contact identifier (UUID or email),
// escaping it so an email address can be passed unencoded.
func contactPath(identifier string) string {
return "/api/contacts/" + url.PathEscape(identifier)
}
96 changes: 96 additions & 0 deletions contacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package mailtrap_test

import (
"context"
"net/http"
"testing"

"github.com/mailtrap/mailtrap-go"
)

func TestContacts_Create(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("POST /api/contacts", func(w http.ResponseWriter, r *http.Request) {
wantJSONBody(t, r, `{"contact":{"email":"john@example.com","fields":{"first_name":"John"},"list_ids":[1,2]}}`)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"data":{"id":"018dd5e3","email":"john@example.com","status":"subscribed","list_ids":[1,2]}}`))
})

contact, _, err := client.Contacts.Create(context.Background(), &mailtrap.CreateContactRequest{
Email: "john@example.com",
Fields: map[string]any{"first_name": "John"},
ListIDs: []int64{1, 2},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if contact.ID != "018dd5e3" || contact.Status != mailtrap.ContactStatusSubscribed {
t.Errorf("contact = %+v", contact)
}
}

func TestContacts_Get(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("GET /api/contacts/018dd5e3", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"data":{"id":"018dd5e3","email":"john@example.com","status":"unsubscribed"}}`))
})

contact, _, err := client.Contacts.Get(context.Background(), "018dd5e3")
if err != nil {
t.Fatalf("Get: %v", err)
}
if contact.Email != "john@example.com" || contact.Status != mailtrap.ContactStatusUnsubscribed {
t.Errorf("contact = %+v", contact)
}
}

func TestContacts_Get_byEmail(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("GET /api/contacts/john@example.com", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"data":{"id":"018dd5e3","email":"john@example.com"}}`))
})

contact, _, err := client.Contacts.Get(context.Background(), "john@example.com")
if err != nil {
t.Fatalf("Get by email: %v", err)
}
if contact.ID != "018dd5e3" {
t.Errorf("contact = %+v", contact)
}
}

func TestContacts_Update(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("PATCH /api/contacts/018dd5e3", func(w http.ResponseWriter, r *http.Request) {
wantJSONBody(t, r, `{"contact":{"email":"john@example.com","list_ids_included":[3],"list_ids_excluded":[1],"unsubscribed":true}}`)
_, _ = w.Write([]byte(`{"action":"updated","data":{"id":"018dd5e3","email":"john@example.com","status":"unsubscribed"}}`))
})

upsert, _, err := client.Contacts.Update(context.Background(), "018dd5e3", &mailtrap.UpdateContactRequest{
Email: "john@example.com",
ListIDsIncluded: []int64{3},
ListIDsExcluded: []int64{1},
Unsubscribed: mailtrap.Ptr(true),
})
if err != nil {
t.Fatalf("Update: %v", err)
}
if upsert.Action != "updated" || upsert.Contact.ID != "018dd5e3" {
t.Errorf("upsert = %+v (contact %+v)", upsert, upsert.Contact)
}
}

func TestContacts_Delete(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("DELETE /api/contacts/018dd5e3", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})

resp, err := client.Contacts.Delete(context.Background(), "018dd5e3")
if err != nil {
t.Fatalf("Delete: %v", err)
}
if resp.StatusCode != http.StatusNoContent {
t.Errorf("status = %d", resp.StatusCode)
}
}
36 changes: 36 additions & 0 deletions examples/contact-events/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/mailtrap/mailtrap-go"
)

func main() {
client, err := mailtrap.NewClient(os.Getenv("MAILTRAP_API_TOKEN"))
if err != nil {
log.Fatal(err)
}

ctx := context.Background()

contact, _, err := client.Contacts.Create(ctx, &mailtrap.CreateContactRequest{
Email: "john.smith@example.com",
})
if err != nil {
log.Fatal(err)
}

// Record a custom event for the contact (by UUID or email).
event, _, err := client.ContactEvents.Create(ctx, contact.ID, &mailtrap.CreateContactEventRequest{
Name: "UserLogin",
Params: map[string]any{"user_id": 101, "is_active": true},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("recorded event %q for %s\n", event.Name, event.ContactEmail)
}
52 changes: 52 additions & 0 deletions examples/contacts/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/mailtrap/mailtrap-go"
)

func main() {
client, err := mailtrap.NewClient(os.Getenv("MAILTRAP_API_TOKEN"))
if err != nil {
log.Fatal(err)
}

ctx := context.Background()

contact, _, err := client.Contacts.Create(ctx, &mailtrap.CreateContactRequest{
Email: "john.smith@example.com",
Fields: map[string]any{"first_name": "John", "last_name": "Smith"},
ListIDs: []int64{1, 2, 3},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("created contact %s (%s)\n", contact.ID, contact.Status)

// Get by UUID or email (the email is URL-encoded for you).
got, _, err := client.Contacts.Get(ctx, contact.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("contact email: %s\n", got.Email)

// Update is an upsert; it reports whether the contact was created or updated.
upsert, _, err := client.Contacts.Update(ctx, contact.ID, &mailtrap.UpdateContactRequest{
Email: "john.smith@example.com",
Fields: map[string]any{"first_name": "Johnny"},
Unsubscribed: mailtrap.Ptr(true),
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("contact %s\n", upsert.Action)

if _, err := client.Contacts.Delete(ctx, contact.ID); err != nil {
log.Fatal(err)
}
fmt.Println("deleted contact")
}