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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0] - 2025-01-16

### Added
- **Webhook Support**: Complete webhook payload parsing for inbound emails
- `WebhookPayload` type matching official Inbound documentation structure
- Support for `email.received` webhook events with nested email data
- `WebhookEmailData`, `WebhookParsedData`, and `WebhookCleanedContent` types
- `WebhookAddressGroup` and `WebhookAddress` for email address handling
- `WebhookAttachment` type for email attachments in webhooks
- `ParseWebhookPayload()` function for parsing incoming webhook requests
- Helper methods: `GetFromAddress()`, `GetToAddress()`, `GetHeaders()`
- Support for both `parsedData` and `cleanedContent` from webhook payloads
- Complex header parsing (strings, arrays, objects like DKIM signatures)
- Comprehensive webhook parsing tests with edge case coverage

### Technical Details
- Added `webhook.go` with webhook parsing utilities
- Added `webhook_test.go` with comprehensive test coverage
- Updated type definitions to match official Inbound webhook structure
- Support for flexible date handling (string or Date object)
- Proper handling of optional fields and null values

## [0.1.0] - 2024-01-XX

### Added
Expand Down
70 changes: 70 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,73 @@ type DeleteScheduledEmailResponse struct {
Status string `json:"status"` // 'cancelled'
CancelledAt string `json:"cancelled_at"`
}

// Webhook Payload Types - for incoming email.received webhooks
type WebhookPayload struct {
Event string `json:"event"`
Timestamp string `json:"timestamp"`
Email WebhookEmailData `json:"email"`
Endpoint *WebhookEndpointRef `json:"endpoint,omitempty"`
}

type WebhookEmailData struct {
ID string `json:"id"`
MessageID string `json:"messageId"`
From WebhookAddressGroup `json:"from"`
To WebhookAddressGroup `json:"to"`
Recipient string `json:"recipient"`
Subject string `json:"subject"`
ReceivedAt string `json:"receivedAt"`
ParsedData WebhookParsedData `json:"parsedData"`
CleanedContent *WebhookCleanedContent `json:"cleanedContent,omitempty"`
}

type WebhookAddressGroup struct {
Text string `json:"text"`
Addresses []WebhookAddress `json:"addresses"`
}

type WebhookAddress struct {
Name *string `json:"name"`
Address string `json:"address"`
}

type WebhookParsedData struct {
MessageID string `json:"messageId"`
Date any `json:"date"` // Can be string or Date object
Subject string `json:"subject"`
From WebhookAddressGroup `json:"from"`
To WebhookAddressGroup `json:"to"`
Cc *WebhookAddressGroup `json:"cc"`
Bcc *WebhookAddressGroup `json:"bcc"`
ReplyTo *WebhookAddressGroup `json:"replyTo"`
InReplyTo *string `json:"inReplyTo,omitempty"`
References *string `json:"references,omitempty"`
TextBody string `json:"textBody"`
HTMLBody string `json:"htmlBody"`
Attachments []WebhookAttachment `json:"attachments"`
Headers map[string]any `json:"headers"`
Priority *string `json:"priority,omitempty"`
}

type WebhookCleanedContent struct {
HTML string `json:"html"`
Text string `json:"text"`
HasHTML bool `json:"hasHtml"`
HasText bool `json:"hasText"`
Attachments []WebhookAttachment `json:"attachments"`
Headers map[string]any `json:"headers"`
}

type WebhookAttachment struct {
Filename string `json:"filename"`
ContentType string `json:"contentType"`
ContentID string `json:"contentId"`
URL string `json:"url"`
}

type WebhookEndpointRef struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
73 changes: 73 additions & 0 deletions webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package inboundgo

import (
"encoding/json"
"fmt"
"io"
)

// ParseWebhookPayload parses an incoming webhook payload into the WebhookPayload struct
func ParseWebhookPayload(reader io.Reader) (*WebhookPayload, error) {
var payload WebhookPayload
decoder := json.NewDecoder(reader)
err := decoder.Decode(&payload)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook payload: %w", err)
}
return &payload, nil
}

// GetFromAddress extracts the properly formatted from address from the webhook
func (w *WebhookPayload) GetFromAddress() string {
if len(w.Email.From.Addresses) > 0 {
addr := w.Email.From.Addresses[0]
if addr.Name != nil && *addr.Name != "" {
return fmt.Sprintf("%s <%s>", *addr.Name, addr.Address)
}
return addr.Address
}
return ""
}

// GetToAddress extracts the properly formatted to address from the webhook
func (w *WebhookPayload) GetToAddress() string {
if len(w.Email.To.Addresses) > 0 {
addr := w.Email.To.Addresses[0]
if addr.Name != nil && *addr.Name != "" {
return fmt.Sprintf("%s <%s>", *addr.Name, addr.Address)
}
return addr.Address
}
return ""
}

// GetHeaders converts the headers from the webhook format to a standard map[string][]string format
func (w *WebhookPayload) GetHeaders() map[string][]string {
headers := make(map[string][]string)
for k, v := range w.Email.ParsedData.Headers {
switch val := v.(type) {
case string:
headers[k] = []string{val}
case []string:
headers[k] = val
case []any:
var strSlice []string
for _, item := range val {
if str, ok := item.(string); ok {
strSlice = append(strSlice, str)
}
}
if len(strSlice) > 0 {
headers[k] = strSlice
}
case map[string]any:
// Handle complex header structures like dkim-signature
if text, ok := val["text"].(string); ok {
headers[k] = []string{text}
} else if value, ok := val["value"].(string); ok {
headers[k] = []string{value}
}
}
}
return headers
}
Loading
Loading