From be77490bdcd2297bb75ebec140945a23acea53e3 Mon Sep 17 00:00:00 2001 From: huijiro Date: Tue, 21 Oct 2025 17:58:00 -0300 Subject: [PATCH] Fixes typing to match JS SDK --- attachment_test.go | 22 ++++++--- inbound.go | 27 +++++----- types.go | 121 ++++++++++++++++++++++++--------------------- webhook.go | 18 ++++--- webhook_test.go | 82 +++++++++++++++++------------- 5 files changed, 156 insertions(+), 114 deletions(-) diff --git a/attachment_test.go b/attachment_test.go index 709d4ec..375d0ac 100644 --- a/attachment_test.go +++ b/attachment_test.go @@ -42,7 +42,7 @@ func TestAttachmentDownload(t *testing.T) { filename: "document.pdf", serverStatus: http.StatusNotFound, expectError: true, - errorContains: "Error occurred", + errorContains: "404", }, { name: "attachment not found", @@ -50,7 +50,7 @@ func TestAttachmentDownload(t *testing.T) { filename: "missing.pdf", serverStatus: http.StatusNotFound, expectError: true, - errorContains: "Error occurred", + errorContains: "404", }, { name: "unauthorized", @@ -58,7 +58,7 @@ func TestAttachmentDownload(t *testing.T) { filename: "document.pdf", serverStatus: http.StatusUnauthorized, expectError: true, - errorContains: "Error occurred", + errorContains: "401", }, } @@ -103,7 +103,7 @@ func TestAttachmentDownload(t *testing.T) { t.Fatalf("Failed to create client: %v", err) } - data, err := client.Attachment().Download(context.Background(), tt.emailID, tt.filename) + result, err := client.Attachment().Download(context.Background(), tt.emailID, tt.filename) if tt.expectError { if err == nil { @@ -121,9 +121,17 @@ func TestAttachmentDownload(t *testing.T) { return } - if string(data) != string(tt.serverResponse) { - t.Errorf("Expected data '%s', got '%s'", string(tt.serverResponse), string(data)) + if string(result.Data) != string(tt.serverResponse) { + t.Errorf("Expected data '%s', got '%s'", string(tt.serverResponse), string(result.Data)) + } + + // Verify headers are present + if result.Headers == nil { + t.Error("Expected headers to be present") + } + if result.Headers.Get("Content-Type") != "application/pdf" { + t.Errorf("Expected Content-Type 'application/pdf', got '%s'", result.Headers.Get("Content-Type")) } }) } -} +} \ No newline at end of file diff --git a/inbound.go b/inbound.go index 7718324..1425f32 100644 --- a/inbound.go +++ b/inbound.go @@ -301,7 +301,7 @@ func NewEmailService(client *Inbound) *EmailService { } // Send sends an email with optional attachments and idempotency options -// +// // This method supports both immediate sending and scheduled delivery. // If params.ScheduledAt is set, the email will be scheduled for future delivery. // @@ -345,7 +345,7 @@ func (s *EmailService) Reply(ctx context.Context, id string, params *PostEmailRe } // Schedule schedules an email to be sent at a future time -// +// // Supports both ISO 8601 dates and natural language (e.g., "in 1 hour", "tomorrow at 9am"). // // API Reference: https://docs.inbound.new/api-reference/emails/schedule-email @@ -625,27 +625,28 @@ func NewAttachmentService(client *Inbound) *AttachmentService { // Download downloads an email attachment by email ID and filename // // API Reference: https://docs.inbound.new/api-reference/attachments/download-attachment -func (s *AttachmentService) Download(ctx context.Context, emailID, filename string) ([]byte, error) { +func (s *AttachmentService) Download(ctx context.Context, emailID, filename string) (*AttachmentDownloadResponse, error) { endpoint := fmt.Sprintf("/attachments/%s/%s", emailID, url.PathEscape(filename)) - + resp, err := s.client.request(ctx, "GET", endpoint, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode >= 400 { - respBody, _ := io.ReadAll(resp.Body) - var errorResp struct { - Error string `json:"error"` - } - if json.Unmarshal(respBody, &errorResp) == nil && errorResp.Error != "" { - return nil, fmt.Errorf("%s", errorResp.Error) - } return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } - return io.ReadAll(resp.Body) + return &AttachmentDownloadResponse{ + Data: data, + Headers: resp.Header, + }, nil } // Add service properties to the main client @@ -763,4 +764,4 @@ func Int(v int) *int { // Bool returns a pointer to the bool value passed in. func Bool(v bool) *bool { return &v -} \ No newline at end of file +} diff --git a/types.go b/types.go index 6a8d358..6edc978 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,9 @@ package inboundgo -import "time" +import ( + "net/http" + "time" +) // Base configuration type InboundEmailConfig struct { @@ -583,16 +586,16 @@ type ThreadLatestMessage struct { } type ThreadSummary struct { - ID string `json:"id"` - RootMessageID string `json:"rootMessageId"` - NormalizedSubject *string `json:"normalizedSubject"` - ParticipantEmails []string `json:"participantEmails"` - MessageCount int `json:"messageCount"` - LastMessageAt string `json:"lastMessageAt"` - CreatedAt string `json:"createdAt"` - HasUnread bool `json:"hasUnread"` - IsArchived bool `json:"isArchived"` - LatestMessage *ThreadLatestMessage `json:"latestMessage,omitempty"` + ID string `json:"id"` + RootMessageID string `json:"rootMessageId"` + NormalizedSubject *string `json:"normalizedSubject"` + ParticipantEmails []string `json:"participantEmails"` + MessageCount int `json:"messageCount"` + LastMessageAt string `json:"lastMessageAt"` + CreatedAt string `json:"createdAt"` + HasUnread bool `json:"hasUnread"` + IsArchived bool `json:"isArchived"` + LatestMessage *ThreadLatestMessage `json:"latestMessage,omitempty"` } type GetThreadsRequest struct { @@ -620,11 +623,11 @@ type GetThreadsResponse struct { } type ThreadAttachment struct { - Filename string `json:"filename"` - ContentType string `json:"contentType"` - Size int `json:"size"` - ContentID string `json:"contentId"` - ContentDisposition string `json:"contentDisposition"` + Filename string `json:"filename"` + ContentType string `json:"contentType"` + Size int `json:"size"` + ContentID string `json:"contentId"` + ContentDisposition string `json:"contentDisposition"` } type ThreadMessage struct { @@ -712,33 +715,33 @@ type MostActiveThread struct { } type GetThreadStatsResponse struct { - TotalThreads int `json:"totalThreads"` - TotalMessages int `json:"totalMessages"` - AverageMessagesPerThread float64 `json:"averageMessagesPerThread"` - MostActiveThread *MostActiveThread `json:"mostActiveThread"` - RecentActivity ThreadRecentActivity `json:"recentActivity"` - Distribution ThreadDistribution `json:"distribution"` - UnreadStats ThreadUnreadStats `json:"unreadStats"` + TotalThreads int `json:"totalThreads"` + TotalMessages int `json:"totalMessages"` + AverageMessagesPerThread float64 `json:"averageMessagesPerThread"` + MostActiveThread *MostActiveThread `json:"mostActiveThread"` + RecentActivity ThreadRecentActivity `json:"recentActivity"` + Distribution ThreadDistribution `json:"distribution"` + UnreadStats ThreadUnreadStats `json:"unreadStats"` } // 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"` + Event string `json:"event"` + Timestamp string `json:"timestamp"` + Email WebhookEmailData `json:"email"` + Endpoint WebhookEndpointRef `json:"endpoint"` } 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"` + 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"` } type WebhookAddressGroup struct { @@ -748,31 +751,31 @@ type WebhookAddressGroup struct { type WebhookAddress struct { Name *string `json:"name"` - Address string `json:"address"` + 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"` + MessageID *string `json:"messageId,omitempty"` + Date *string `json:"date,omitempty"` + Subject *string `json:"subject,omitempty"` + 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"` - Raw string `json:"raw"` + References []string `json:"references,omitempty"` + TextBody *string `json:"textBody,omitempty"` + HTMLBody *string `json:"htmlBody,omitempty"` + Raw *string `json:"raw,omitempty"` Attachments []WebhookAttachment `json:"attachments"` Headers map[string]any `json:"headers"` - Priority *string `json:"priority,omitempty"` + Priority any `json:"priority,omitempty"` // Can be string | false | undefined } type WebhookCleanedContent struct { - HTML string `json:"html"` - Text string `json:"text"` + HTML *string `json:"html"` + Text *string `json:"text"` HasHTML bool `json:"hasHtml"` HasText bool `json:"hasText"` Attachments []WebhookAttachment `json:"attachments"` @@ -780,12 +783,12 @@ type WebhookCleanedContent struct { } type WebhookAttachment struct { - Filename string `json:"filename"` - ContentType string `json:"contentType"` - Size int `json:"size"` - ContentID string `json:"contentId"` - ContentDisposition string `json:"contentDisposition"` - DownloadUrl string `json:"downloadUrl"` + Filename *string `json:"filename,omitempty"` + ContentType *string `json:"contentType,omitempty"` + Size *int `json:"size,omitempty"` + ContentID *string `json:"contentId,omitempty"` + ContentDisposition *string `json:"contentDisposition,omitempty"` + DownloadUrl string `json:"downloadUrl"` } type WebhookEndpointRef struct { @@ -793,3 +796,11 @@ type WebhookEndpointRef struct { Name string `json:"name"` Type string `json:"type"` } + +// ---- Attachment Types ---- + +// AttachmentDownloadResponse represents the response from downloading an attachment. +type AttachmentDownloadResponse struct { + Data []byte `json:"data"` + Headers http.Header `json:"headers"` +} diff --git a/webhook.go b/webhook.go index 26eb2e7..1d106d0 100644 --- a/webhook.go +++ b/webhook.go @@ -19,24 +19,30 @@ func ParseWebhookPayload(reader io.Reader) (*WebhookPayload, error) { // GetFromAddress extracts the properly formatted from address from the webhook func (w *WebhookPayload) GetFromAddress() string { - if len(w.Email.From.Addresses) > 0 { + if w.Email.From != nil && len(w.Email.From.Addresses) > 0 { addr := w.Email.From.Addresses[0] + if addr.Address == nil { + return "" + } if addr.Name != nil && *addr.Name != "" { - return fmt.Sprintf("%s <%s>", *addr.Name, addr.Address) + return fmt.Sprintf("%s <%s>", *addr.Name, *addr.Address) } - return 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 { + if w.Email.To != nil && len(w.Email.To.Addresses) > 0 { addr := w.Email.To.Addresses[0] + if addr.Address == nil { + return "" + } if addr.Name != nil && *addr.Name != "" { - return fmt.Sprintf("%s <%s>", *addr.Name, addr.Address) + return fmt.Sprintf("%s <%s>", *addr.Name, *addr.Address) } - return addr.Address + return *addr.Address } return "" } diff --git a/webhook_test.go b/webhook_test.go index 2692c77..e165878 100644 --- a/webhook_test.go +++ b/webhook_test.go @@ -111,12 +111,20 @@ func TestParseWebhookPayload(t *testing.T) { t.Errorf("Expected email ID '7U6TcAy-16qmzu297IVoL', got '%s'", webhook.Email.ID) } - if webhook.Email.MessageID != "" { - t.Errorf("Expected message ID '', got '%s'", webhook.Email.MessageID) + if webhook.Email.MessageID == nil || *webhook.Email.MessageID != "" { + if webhook.Email.MessageID == nil { + t.Error("Expected message ID to be present") + } else { + t.Errorf("Expected message ID '', got '%s'", *webhook.Email.MessageID) + } } - if webhook.Email.Subject != "Test Email - Inbound Email Service" { - t.Errorf("Expected subject 'Test Email - Inbound Email Service', got '%s'", webhook.Email.Subject) + if webhook.Email.Subject == nil || *webhook.Email.Subject != "Test Email - Inbound Email Service" { + if webhook.Email.Subject == nil { + t.Error("Expected subject to be present") + } else { + t.Errorf("Expected subject 'Test Email - Inbound Email Service', got '%s'", *webhook.Email.Subject) + } } if webhook.Email.Recipient != "test@yourdomain.com" { @@ -135,33 +143,45 @@ func TestParseWebhookPayload(t *testing.T) { } // Test parsed data - if webhook.Email.ParsedData.TextBody != "This is a test email.\nRendered for webhook testing." { - t.Errorf("Expected text body 'This is a test email.\\nRendered for webhook testing.', got '%s'", webhook.Email.ParsedData.TextBody) + if webhook.Email.ParsedData.TextBody == nil || *webhook.Email.ParsedData.TextBody != "This is a test email.\nRendered for webhook testing." { + if webhook.Email.ParsedData.TextBody == nil { + t.Error("Expected text body to be present") + } else { + t.Errorf("Expected text body 'This is a test email.\\nRendered for webhook testing.', got '%s'", *webhook.Email.ParsedData.TextBody) + } } - if webhook.Email.ParsedData.HTMLBody != "

This is a test email.

Rendered for webhook testing.

" { - t.Errorf("Expected HTML body '

This is a test email.

Rendered for webhook testing.

', got '%s'", webhook.Email.ParsedData.HTMLBody) + if webhook.Email.ParsedData.HTMLBody == nil || *webhook.Email.ParsedData.HTMLBody != "

This is a test email.

Rendered for webhook testing.

" { + if webhook.Email.ParsedData.HTMLBody == nil { + t.Error("Expected HTML body to be present") + } else { + t.Errorf("Expected HTML body '

This is a test email.

Rendered for webhook testing.

', got '%s'", *webhook.Email.ParsedData.HTMLBody) + } } // Test cleaned content - if webhook.Email.CleanedContent == nil { - t.Error("Expected cleaned content to be present") - } else { - if webhook.Email.CleanedContent.Text != "This is a test email.\nRendered for webhook testing." { - t.Errorf("Expected cleaned content text 'This is a test email.\\nRendered for webhook testing.', got '%s'", webhook.Email.CleanedContent.Text) + if webhook.Email.CleanedContent.Text == nil || *webhook.Email.CleanedContent.Text != "This is a test email.\nRendered for webhook testing." { + if webhook.Email.CleanedContent.Text == nil { + t.Error("Expected cleaned content text to be present") + } else { + t.Errorf("Expected cleaned content text 'This is a test email.\\nRendered for webhook testing.', got '%s'", *webhook.Email.CleanedContent.Text) } + } - if webhook.Email.CleanedContent.HTML != "

This is a test email.

Rendered for webhook testing.

" { - t.Errorf("Expected cleaned content HTML '

This is a test email.

Rendered for webhook testing.

', got '%s'", webhook.Email.CleanedContent.HTML) + if webhook.Email.CleanedContent.HTML == nil || *webhook.Email.CleanedContent.HTML != "

This is a test email.

Rendered for webhook testing.

" { + if webhook.Email.CleanedContent.HTML == nil { + t.Error("Expected cleaned content HTML to be present") + } else { + t.Errorf("Expected cleaned content HTML '

This is a test email.

Rendered for webhook testing.

', got '%s'", *webhook.Email.CleanedContent.HTML) } + } - if !webhook.Email.CleanedContent.HasHTML { - t.Error("Expected cleaned content to have HTML") - } + if !webhook.Email.CleanedContent.HasHTML { + t.Error("Expected cleaned content to have HTML") + } - if !webhook.Email.CleanedContent.HasText { - t.Error("Expected cleaned content to have text") - } + if !webhook.Email.CleanedContent.HasText { + t.Error("Expected cleaned content to have text") } // Test headers parsing @@ -180,20 +200,16 @@ func TestParseWebhookPayload(t *testing.T) { } // Test endpoint - if webhook.Endpoint == nil { - t.Error("Expected endpoint to be present") - } else { - if webhook.Endpoint.ID != "LHbWZ1iEOofDXlViXWsDH" { - t.Errorf("Expected endpoint ID 'LHbWZ1iEOofDXlViXWsDH', got '%s'", webhook.Endpoint.ID) - } + if webhook.Endpoint.ID != "LHbWZ1iEOofDXlViXWsDH" { + t.Errorf("Expected endpoint ID 'LHbWZ1iEOofDXlViXWsDH', got '%s'", webhook.Endpoint.ID) + } - if webhook.Endpoint.Name != "6979012cd152 E" { - t.Errorf("Expected endpoint name '6979012cd152 E', got '%s'", webhook.Endpoint.Name) - } + if webhook.Endpoint.Name != "6979012cd152 E" { + t.Errorf("Expected endpoint name '6979012cd152 E', got '%s'", webhook.Endpoint.Name) + } - if webhook.Endpoint.Type != "webhook" { - t.Errorf("Expected endpoint type 'webhook', got '%s'", webhook.Endpoint.Type) - } + if webhook.Endpoint.Type != "webhook" { + t.Errorf("Expected endpoint type 'webhook', got '%s'", webhook.Endpoint.Type) } }