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
10 changes: 10 additions & 0 deletions src/Client/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ public function put(string $path, array $data, array $headers = []): mixed
return $this->request('put', $path, ['json' => $data], $headers);
}

/**
* @param RequestBody $data
* @param RequestHeaders $headers
* @return ApiResponse
*/
public function patch(string $path, array $data, array $headers = []): mixed
{
return $this->request('patch', $path, ['json' => $data], $headers);
}

/**
* @param RequestBody $query Query parameters
* @param RequestHeaders $headers Additional headers for this request
Expand Down
8 changes: 8 additions & 0 deletions src/Endpoints/EmailEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ public function subject(string $subject): self
return $this;
}

/** Set the requested delivery time for the email. */
public function scheduledAt(string $scheduledAt): self
{
$this->payload['scheduled_at'] = $scheduledAt;

return $this;
}

/**
* Set the HTML body of the email.
*
Expand Down
10 changes: 10 additions & 0 deletions src/Endpoints/Endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ protected function putArray(string $path, array $data = [], array $headers = [])
return $this->expectArray($this->httpClient->put($path, $data, $headers));
}

/**
* @param array<array-key, mixed> $data
* @param array<string, string> $headers
* @return array<array-key, mixed>
*/
protected function patchArray(string $path, array $data = [], array $headers = []): array
{
return $this->expectArray($this->httpClient->patch($path, $data, $headers));
}

/**
* @param array<array-key, mixed> $query
* @return array<array-key, mixed>
Expand Down
12 changes: 12 additions & 0 deletions src/Endpoints/MessagesEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Lettermint\Responses\MessageEventsResponse;
use Lettermint\Responses\MessageListResponse;
use Lettermint\Responses\MessageResponse;
use Lettermint\Responses\ScheduledMessageResponse;

class MessagesEndpoint extends Endpoint
{
Expand All @@ -18,6 +19,17 @@ public function retrieve(string $messageId): MessageResponse
return $this->hydrate(MessageResponse::class, $this->getArray($this->path('/messages/{messageId}', ['messageId' => $messageId]), []));
}

/** @param array{scheduled_at: string} $data */
public function reschedule(string $messageId, array $data): ScheduledMessageResponse
{
return $this->hydrate(ScheduledMessageResponse::class, $this->patchArray($this->path('/messages/{messageId}', ['messageId' => $messageId]), $data));
}

public function cancel(string $messageId): ScheduledMessageResponse
{
return $this->hydrate(ScheduledMessageResponse::class, $this->postArray($this->path('/messages/{messageId}/cancel', ['messageId' => $messageId])));
}

public function events(string $messageId, array $query = []): MessageEventsResponse
{
return $this->hydrate(MessageEventsResponse::class, $this->getArray($this->path('/messages/{messageId}/events', ['messageId' => $messageId]), $query));
Expand Down
15 changes: 8 additions & 7 deletions src/Objects/MessageData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@
/**
* @property string $id
* @property 'inbound'|'outbound' $type
* @property 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed' $status
* @property 'scheduled'|'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'canceled' $status
* @property string|null $status_changed_at
* @property string|null $scheduled_at
* @property string|null $tag
* @property string $from_email
* @property string|null $from_name
* @property list<string>|null $reply_to
* @property string|null $subject
* @property list<\Lettermint\Objects\MessageRecipientData>|null $to
* @property list<\Lettermint\Objects\MessageRecipientData>|null $cc
* @property list<\Lettermint\Objects\MessageRecipientData>|null $bcc
* @property list<\Lettermint\Objects\MessageAttachmentData>|null $attachments
* @property list<MessageRecipientData>|null $to
* @property list<MessageRecipientData>|null $cc
* @property list<MessageRecipientData>|null $bcc
* @property list<MessageAttachmentData>|null $attachments
* @property array<string, string>|null $metadata
* @property float|int|null $spam_score
* @property list<\Lettermint\Objects\SpamSymbol> $spam_symbols
* @property list<SpamSymbol> $spam_symbols
* @property string $route_id
* @property string $created_at
*/
final class MessageData extends Resource
{
protected static array $casts = [
'spam_symbols' => [\Lettermint\Objects\SpamSymbol::class],
'spam_symbols' => [SpamSymbol::class],
];
}
2 changes: 1 addition & 1 deletion src/Objects/MessageEventData.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/**
* @property string $message_id
* @property 'queued'|'processed'|'suppressed'|'delivered'|'auto_replied'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'opened'|'clicked'|'inbound_received'|'inbound_queued'|'inbound_spam_blocked'|'inbound_processed'|'inbound_retry' $event
* @property 'scheduled'|'rescheduled'|'canceled'|'released'|'queued'|'processed'|'suppressed'|'delivered'|'auto_replied'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'opened'|'clicked'|'inbound_received'|'inbound_queued'|'inbound_spam_blocked'|'inbound_processed'|'inbound_retry' $event
* @property array<string, mixed>|null $metadata
* @property string $timestamp
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Objects/MessageListData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
/**
* @property string $id
* @property 'inbound'|'outbound' $type
* @property 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed' $status
* @property 'scheduled'|'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'canceled' $status
* @property string|null $scheduled_at
* @property float|int|null $spam_score
* @property string $from_email
* @property string|null $from_name
Expand Down
1 change: 1 addition & 0 deletions src/Objects/SendMailRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @property string $route
* @property string $from
* @property string $subject
* @property string $scheduled_at
* @property string|null $tag
* @property string|null $html
* @property string|null $text
Expand Down
15 changes: 15 additions & 0 deletions src/Responses/ScheduledMessageResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Lettermint\Responses;

use Lettermint\Resource;

/**
* @property string $message_id
* @property 'scheduled'|'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'canceled'|null $status
* @property string|null $scheduled_at
*/
final class ScheduledMessageResponse extends Resource
{
//
}
3 changes: 2 additions & 1 deletion src/Responses/SendMailResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

/**
* @property string $message_id
* @property 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed' $status
* @property 'scheduled'|'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'canceled' $status
* @property string $scheduled_at
*/
final class SendMailResponse extends Resource
{
Expand Down
14 changes: 8 additions & 6 deletions src/Types/ApiTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*
* @phpstan-type ApiObject array<string, mixed>
* @phpstan-type CursorPage array{data: list<ApiObject>, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null}
* @phpstan-type MessageStatus 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'
* @phpstan-type SendMailRequest array{route?: string, from: string, to: non-empty-list<string>, cc?: list<string>, bcc?: list<string>, reply_to?: list<string>, subject: string, headers?: array<string, string>, metadata?: array<string, string>, tag?: string|null, tags?: list<array{name: string, value: string}>, settings?: array{track_opens?: bool, track_clicks?: bool, tls?: TlsPolicy}|null, html?: string|null, text?: string|null, attachments?: list<array{filename: string, content: string, content_type?: string|null, content_id?: string|null}>}
* @phpstan-type MessageStatus 'scheduled'|'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'canceled'
* @phpstan-type SendMailRequest array{route?: string, from: string, to: non-empty-list<string>, cc?: list<string>, bcc?: list<string>, reply_to?: list<string>, subject: string, scheduled_at?: string, headers?: array<string, string>, metadata?: array<string, string>, tag?: string|null, tags?: list<array{name: string, value: string}>, settings?: array{track_opens?: bool, track_clicks?: bool, tls?: TlsPolicy}|null, html?: string|null, text?: string|null, attachments?: list<array{filename: string, content: string, content_type?: string|null, content_id?: string|null}>}
* @phpstan-type SendBatchMailRequest list<SendMailRequest>
* @phpstan-type TlsPolicy 'opportunistic'|'enforced'
* @phpstan-type AttachmentDelivery 'inline'|'url'
Expand All @@ -24,10 +24,10 @@
* @phpstan-type DomainStatus 'verified'|'partially_verified'|'pending_verification'|'failed_verification'
* @phpstan-type InitialRoutes 'both'|'transactional'|'broadcast'
* @phpstan-type MessageAttachmentData array{size: int, filename: string, content_id: string|null, content_type: string}
* @phpstan-type MessageData array{id: string, type: MessageType, status: MessageStatus, status_changed_at: string|null, tag: string|null, tags: list<array{name: string, value: string}>, from_email: string, from_name: string|null, reply_to: list<string>|null, subject: string|null, to: list<MessageRecipientData>|null, cc: list<MessageRecipientData>|null, bcc: list<MessageRecipientData>|null, attachments: list<MessageAttachmentData>|null, metadata: array<string, string>|null, spam_score?: float|int|null, spam_symbols?: list<SpamSymbol>, route_id: string, created_at: string}
* @phpstan-type MessageData array{id: string, type: MessageType, status: MessageStatus, status_changed_at: string|null, scheduled_at: string|null, tag: string|null, tags: list<array{name: string, value: string}>, from_email: string, from_name: string|null, reply_to: list<string>|null, subject: string|null, to: list<MessageRecipientData>|null, cc: list<MessageRecipientData>|null, bcc: list<MessageRecipientData>|null, attachments: list<MessageAttachmentData>|null, metadata: array<string, string>|null, spam_score?: float|int|null, spam_symbols?: list<SpamSymbol>, route_id: string, created_at: string}
* @phpstan-type MessageEventData array{message_id: string, event: MessageEventType, tag: string|null, tags: list<array{name: string, value: string}>, metadata: array<string, mixed>|null, timestamp: string}
* @phpstan-type MessageEventType 'queued'|'processed'|'suppressed'|'delivered'|'auto_replied'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'opened'|'clicked'|'inbound_received'|'inbound_queued'|'inbound_spam_blocked'|'inbound_processed'|'inbound_retry'
* @phpstan-type MessageListData array{id: string, type: MessageType, status: MessageStatus, spam_score?: float|int|null, from_email: string, from_name: string|null, subject: string|null, to: list<MessageRecipientData>|null, cc: list<MessageRecipientData>|null, bcc: list<MessageRecipientData>|null, reply_to: list<string>|null, tag: string|null, tags: list<array{name: string, value: string}>, status_changed_at: string|null, created_at: string}
* @phpstan-type MessageEventType 'scheduled'|'rescheduled'|'canceled'|'released'|'queued'|'processed'|'suppressed'|'delivered'|'auto_replied'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'opened'|'clicked'|'inbound_received'|'inbound_queued'|'inbound_spam_blocked'|'inbound_processed'|'inbound_retry'
* @phpstan-type MessageListData array{id: string, type: MessageType, status: MessageStatus, scheduled_at: string|null, spam_score?: float|int|null, from_email: string, from_name: string|null, subject: string|null, to: list<MessageRecipientData>|null, cc: list<MessageRecipientData>|null, bcc: list<MessageRecipientData>|null, reply_to: list<string>|null, tag: string|null, tags: list<array{name: string, value: string}>, status_changed_at: string|null, created_at: string}
* @phpstan-type MessageRecipientData array{email: string, name: string|null}
* @phpstan-type MessageStatsData array{messages_transactional: int, messages_broadcast: int, messages_inbound: int, deliverability: float|int}
* @phpstan-type MessageType 'inbound'|'outbound'
Expand Down Expand Up @@ -82,8 +82,10 @@
* @phpstan-type WebhookEvent 'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test'
* @phpstan-type WebhookListData array{id: string, route_id: string, name: string, url: string, events: list<WebhookEvent>, enabled: bool, last_called_at: string|null, created_at: string, updated_at: string}
* @phpstan-type StatsQuery StatsRequestData
* @phpstan-type SendMailResponse array{message_id: string, status: MessageStatus}
* @phpstan-type SendMailResponse array{message_id: string|null, status: MessageStatus, scheduled_at?: string}
* @phpstan-type SendBatchMailResponse list<SendMailResponse>
* @phpstan-type RescheduleMessageRequest array{scheduled_at: string}
* @phpstan-type RescheduleMessageResponse array{message_id: string, status: MessageStatus|null, scheduled_at: string|null}
* @phpstan-type DomainListResponse array{data: list<DomainListData>, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null}
* @phpstan-type DomainResponse DomainData
* @phpstan-type DeleteDomainResponse array{message: string}
Expand Down
8 changes: 8 additions & 0 deletions tests/Endpoints/MessagesEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
expect($this->endpoint->retrieve('message-id')->toArray())->toBe(['data' => ['id' => 'message-id']]);
});

test('it reschedules and cancels scheduled messages', function () {
$this->httpClient->shouldReceive('patch')->once()->with('/v1/messages/message-id', ['scheduled_at' => '2026-08-27T09:00:00Z'], [])->andReturn(['message_id' => 'message-id', 'status' => 'scheduled', 'scheduled_at' => '2026-08-27T09:00:00Z']);
$this->httpClient->shouldReceive('post')->once()->with('/v1/messages/message-id/cancel', [], [])->andReturn(['message_id' => 'message-id', 'status' => 'canceled', 'scheduled_at' => null]);

expect($this->endpoint->reschedule('message-id', ['scheduled_at' => '2026-08-27T09:00:00Z'])->status)->toBe('scheduled');
expect($this->endpoint->cancel('message-id')->status)->toBe('canceled');
});

test('it retrieves message events', function () {
$query = ['include_machine_events' => true];
$this->httpClient->shouldReceive('get')->once()->with('/v1/messages/message-id/events', $query)->andReturn(['data' => []]);
Expand Down
4 changes: 3 additions & 1 deletion tests/SdkCoverageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
['team', 'v1.blockedFileTypes', ApiClient::class, 'blockedFileTypes'],
['team', 'message.index', MessagesEndpoint::class, 'list'],
['team', 'message.show', MessagesEndpoint::class, 'retrieve'],
['team', 'rescheduleMessage', MessagesEndpoint::class, 'reschedule'],
['team', 'cancelScheduledMessage', MessagesEndpoint::class, 'cancel'],
['team', 'message.events', MessagesEndpoint::class, 'events'],
['team', 'message.source', MessagesEndpoint::class, 'source'],
['team', 'message.html', MessagesEndpoint::class, 'html'],
Expand Down Expand Up @@ -65,7 +67,7 @@
['team', 'webhook.showDelivery', WebhooksEndpoint::class, 'delivery'],
];

expect($operations)->toHaveCount(50);
expect($operations)->toHaveCount(52);

$missing = [];

Expand Down