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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ lettermint = Lettermint(
)
```

Each part uses its own token and never falls back to the other one. If the token a method needs is missing, it raises `LettermintConfigError` that names the argument (`domains.list needs team_token; …`), before any request. `lettermint.ping()` uses the team token when it is set, otherwise the sending token. `messages.reschedule()` and `messages.cancel()` accept either token in the same way.
Each part uses its own token and never falls back to the other one. If the token a method needs is missing, it raises `LettermintConfigError` that names the argument (`domains.list needs team_token; …`), before any request. `lettermint.ping()` uses the team token when it is set, otherwise the sending token. `messages.reschedule()`, `messages.cancel()` and `messages.resend()` accept either token in the same way.

You can also pass a single token string. The SDK chooses its type by the format: `lm_team_` followed by letters and digits is a team token, and `lm_` followed by letters and digits is a sending token. Any other value, such as an SSO verification token (`lm_sso_…`), raises `LettermintConfigError`; pass `sending_token=` or `team_token=` explicitly in that case.

Expand Down Expand Up @@ -257,10 +257,17 @@ stats = lettermint.stats.retrieve({"from": "2026-10-01", "to": "2026-10-31"})
html = lettermint.messages.html("message-id")
```

`messages.resend()` sends the stored content of a sent message again, as a new message with its own ID. Leave out the body to resend to every original recipient, or choose some of them. It takes an idempotency key like `emails.send()`. A message can be resent while its content is retained (the API answers `410` otherwise), at most 5 times in 24 hours.

```python
resent = lettermint.messages.resend("message-id", {"recipients": ["jane@example.com"]})
print(resent["message_id"]) # the new message; it links back through resent_from_message_id
```

| Attribute | Methods |
| --- | --- |
| `domains` | `list`, `iterate`, `create`, `retrieve`, `delete`, `verify_dns_records`, `verify_dns_record`, `update_projects` |
| `messages` | `list`, `iterate`, `retrieve`, `events`, `iterate_events`, `source`, `html`, `text`, `reschedule`, `cancel`, `process` |
| `messages` | `list`, `iterate`, `retrieve`, `events`, `iterate_events`, `source`, `html`, `text`, `reschedule`, `cancel`, `process`, `resend` |
| `projects` | `list`, `iterate`, `create`, `retrieve`, `update`, `delete`, `rotate_token` |
| `projects.report_forwarding` | `retrieve`, `update`, `delete`, `verify`, `resend_code` |
| `routes` | `list(project_id)`, `iterate(project_id)`, `create(project_id, …)`, `retrieve`, `update`, `delete`, `verify_inbound_domain` |
Expand Down
40 changes: 35 additions & 5 deletions src/lettermint/_async/resources.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""The Team API sub-clients of :class:`~lettermint.AsyncLettermint`.

Every method uses the team token, except ``messages.reschedule`` and
``messages.cancel``, which use the team token when it is set and otherwise
the sending token.
Every method uses the team token, except ``messages.reschedule``,
``messages.cancel`` and ``messages.resend``, which use the team token when it
is set and otherwise the sending token.
"""

from __future__ import annotations
Expand Down Expand Up @@ -47,13 +47,15 @@
MessageEventData,
MessageListData,
MessageResponse,
PendingSendMailResponse,
ProcessInboundMessageResponse,
ProjectCreatedData,
ProjectData,
ProjectListData,
ProjectMutationResponse,
ReportForwardingRequest,
RescheduleMessageRequest,
ResendMessageRequest,
ResendReportForwardingCodeResponse,
RotateProjectTokenResponse,
RouteData,
Expand Down Expand Up @@ -265,8 +267,8 @@ async def update_projects(
class AsyncMessages(AsyncResource):
"""Sent and received messages. Team token.

``reschedule`` and ``cancel`` also accept the sending token when no team
token is configured.
``reschedule``, ``cancel`` and ``resend`` also accept the sending token when
no team token is configured.
"""

async def list(
Expand Down Expand Up @@ -412,6 +414,34 @@ async def process(
),
)

async def resend(
self,
message_id: str,
body: ResendMessageRequest | None = None,
*,
idempotency_key: str | None = None,
timeout: float | None = None,
) -> PendingSendMailResponse:
"""Sends the stored content of a sent message again, as a new message with its own ID.

Without ``body``, every original recipient receives it again; pass
``{"recipients": [...]}`` to choose some of them. The new message links to
the original through ``resent_from_message_id``. A message can be resent
while its content is retained (otherwise HTTP 410), at most 5 times in 24
hours.
"""
return cast(
PendingSendMailResponse,
await self._request(
"POST /messages/{messageId}/resend",
"messages.resend",
path={"messageId": message_id},
**({} if body is None else {"body": body}),
idempotency_key=idempotency_key,
timeout=timeout,
),
)


class AsyncReportForwarding(AsyncResource):
"""DMARC and complaint report forwarding of a project. Team token."""
Expand Down
6 changes: 3 additions & 3 deletions src/lettermint/_generated/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Generated by lettermint/sdk-generator — do not edit.
# Spec: lettermint/lettermint@bff20c7d86dd05b926713f89c0ad716ef630ae21 (main)
# sending-openapi.json SHA-256: 958303afdf2971b372cab97e72845db2568bc0b6cb6f5c41b2a0dad0386df15d
# team-openapi.json SHA-256: 04718e1e006be90fe586f30754a73a4fd69233119dc36862ded9fb98d69aa138
# Spec: lettermint/lettermint@0f2c60ed7779f89b3960277da896ceffe893eae6 (main)
# sending-openapi.json SHA-256: 8d5e962e57d79ab08be52147c094657b681768f06940a76c8eec1b1e03df54cc
# team-openapi.json SHA-256: e1a0a212f79caf218caa03e9b7f23842b66deba93e2794a49ac02def30803593
# Naming profile: next
"""Generated from the Lettermint OpenAPI specifications. Do not edit; regenerate."""
24 changes: 20 additions & 4 deletions src/lettermint/_generated/operations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated by lettermint/sdk-generator — do not edit.
# Spec: lettermint/lettermint@bff20c7d86dd05b926713f89c0ad716ef630ae21 (main)
# sending-openapi.json SHA-256: 958303afdf2971b372cab97e72845db2568bc0b6cb6f5c41b2a0dad0386df15d
# team-openapi.json SHA-256: 04718e1e006be90fe586f30754a73a4fd69233119dc36862ded9fb98d69aa138
# Spec: lettermint/lettermint@0f2c60ed7779f89b3960277da896ceffe893eae6 (main)
# sending-openapi.json SHA-256: 8d5e962e57d79ab08be52147c094657b681768f06940a76c8eec1b1e03df54cc
# team-openapi.json SHA-256: e1a0a212f79caf218caa03e9b7f23842b66deba93e2794a49ac02def30803593
# Naming profile: next
"""Every operation of the Sending and Team APIs, keyed by ``"<METHOD> <path>"``.

Expand Down Expand Up @@ -689,6 +689,22 @@ class Operation:
pagination=None,
deprecated=False,
),
# Resend a message
"POST /messages/{messageId}/resend": Operation(
operation_id="resendMessage",
method="POST",
path="/messages/{messageId}/resend",
auth="either",
surfaces=("team",),
path_params=("messageId",),
query=None,
headers=("Idempotency-Key",),
request=RequestBody(type="ResendMessageRequest", content_type="application/json", required=False),
response=ResponseBody(status=(202,), content_type="application/json", type="PendingSendMailResponse"),
errors={401: ("ErrorMessage", "ApiErrorBody"), 403: ("ApiErrorBody", "ErrorMessage"), 404: ("ErrorMessage", "ApiErrorBody"), 409: ("ApiErrorBody", "ErrorMessage"), 410: ("ApiErrorBody",), 422: ("ApiErrorBody", "ValidationErrorBody", "ErrorMessage"), 429: ("ApiErrorBody",), 500: ("ApiErrorBody",)},
pagination=None,
deprecated=False,
),
# Create a new project
"POST /projects": Operation(
operation_id="createProject",
Expand Down Expand Up @@ -814,7 +830,7 @@ class Operation:
headers=("Idempotency-Key",),
request=RequestBody(type="SendBatchMailRequest", content_type="application/json", required=True),
response=ResponseBody(status=(202,), content_type="application/json", type="SendBatchMailResponse"),
errors={401: ("ErrorMessage",), 403: ("ApiErrorBody",), 409: ("ErrorMessage",), 422: ("ApiErrorBody", "ValidationErrorBody", "ErrorMessage"), 429: ("ApiErrorBody",), 500: ("ApiErrorBody",)},
errors={401: ("ErrorMessage",), 403: ("ApiErrorBody",), 409: ("ApiErrorBody", "ErrorMessage"), 422: ("ApiErrorBody", "ValidationErrorBody", "ErrorMessage"), 429: ("ApiErrorBody",), 500: ("ApiErrorBody",)},
pagination=None,
deprecated=False,
),
Expand Down
63 changes: 57 additions & 6 deletions src/lettermint/_generated/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated by lettermint/sdk-generator — do not edit.
# Spec: lettermint/lettermint@bff20c7d86dd05b926713f89c0ad716ef630ae21 (main)
# sending-openapi.json SHA-256: 958303afdf2971b372cab97e72845db2568bc0b6cb6f5c41b2a0dad0386df15d
# team-openapi.json SHA-256: 04718e1e006be90fe586f30754a73a4fd69233119dc36862ded9fb98d69aa138
# Spec: lettermint/lettermint@0f2c60ed7779f89b3960277da896ceffe893eae6 (main)
# sending-openapi.json SHA-256: 8d5e962e57d79ab08be52147c094657b681768f06940a76c8eec1b1e03df54cc
# team-openapi.json SHA-256: e1a0a212f79caf218caa03e9b7f23842b66deba93e2794a49ac02def30803593
# Naming profile: next
"""Request and response types of the Lettermint API.

Expand Down Expand Up @@ -162,6 +162,7 @@
"ReportForwardingRequest",
"ReportForwardingResource",
"RescheduleMessageRequest",
"ResendMessageRequest",
"ResendReportForwardingCodeResponse",
"RotateProjectTokenResponse",
"RouteData",
Expand All @@ -173,6 +174,7 @@
"SandboxResult",
"ScheduledMessage",
"ScheduledSendMailResponse",
"SendBatchMailItem",
"SendBatchMailRequest",
"SendBatchMailResponse",
"SendMailRequest",
Expand Down Expand Up @@ -1797,6 +1799,8 @@ class CursorPage(TypedDict, Generic[T]):
"spam_symbols": NotRequired["list[SpamSymbol]"],
"route_id": Required[str],
"created_at": Required[str],
# The ID of the message this message resent. Null when it is not a resend
"resent_from_message_id": NotRequired[str | None],
},
)

Expand Down Expand Up @@ -2133,6 +2137,15 @@ class CursorPage(TypedDict, Generic[T]):
)


ResendMessageRequest = TypedDict(
"ResendMessageRequest",
{
# Recipients of the original message to resend to. Omit to resend to every original recipient.
"recipients": NotRequired[list[str]],
},
)


ResendReportForwardingCodeResponse = TypedDict(
"ResendReportForwardingCodeResponse",
{
Expand Down Expand Up @@ -2282,8 +2295,8 @@ class CursorPage(TypedDict, Generic[T]):
)


SendMailRequest = TypedDict(
"SendMailRequest",
SendBatchMailItem = TypedDict(
"SendBatchMailItem",
{
"route": NotRequired[str],
"from": Required[str],
Expand Down Expand Up @@ -2314,11 +2327,13 @@ class CursorPage(TypedDict, Generic[T]):
"html": NotRequired[str | None],
"text": NotRequired[str | None],
"attachments": NotRequired["list[MessageAttachmentInput]"],
# Optional key (1-255 characters) that makes this email safe to send again, in this or a later batch. If an email with the same key and content was accepted in the last 24 hours, its original result is returned and it is not sent again. Keys must be unique within a batch. Reusing a key with different content, or while the email with that key is still being processed, rejects the whole batch with `409 Conflict` and sends nothing.
"idempotency_key": NotRequired[str],
},
)


SendBatchMailRequest: TypeAlias = list[SendMailRequest]
SendBatchMailRequest: TypeAlias = list[SendBatchMailItem]


SendMailResponse: TypeAlias = PendingSendMailResponse | ScheduledSendMailResponse
Expand All @@ -2327,6 +2342,42 @@ class CursorPage(TypedDict, Generic[T]):
SendBatchMailResponse: TypeAlias = list[SendMailResponse]


SendMailRequest = TypedDict(
"SendMailRequest",
{
"route": NotRequired[str],
"from": Required[str],
"to": Required[list[str]],
"cc": NotRequired[list[str]],
"bcc": NotRequired[list[str]],
"reply_to": NotRequired[list[str]],
"subject": Required[str],
# ISO 8601 or English delivery time. A time without a timezone uses UTC.
"scheduled_at": NotRequired[str],
# The result that a Sandbox project simulates for every recipient.
"sandbox_result": NotRequired["SandboxResult"],
# Custom headers to include in the email. To preserve a submitted RFC Message-ID,
# include both Message-ID and X-LM-Preserve-Message-ID with a string value of true or 1.
# Header names and control values are case-insensitive; false or 0 retains replacement.
# Send values as plain text: Lettermint encodes non-ASCII characters and repairs small formatting
# errors, such as line breaks, control characters and missing angle brackets. Header names must
# be printable ASCII without a colon.
"headers": NotRequired[dict[str, str]],
# Metadata to track with the email (not added as email headers).
"metadata": NotRequired[dict[str, str]],
# Tag to categorize and filter emails (alphanumeric, underscores, hyphens, spaces allowed).
"tag": NotRequired[str | None],
# Reusable exact-match tags. Names must be unique and are case-sensitive.
"tags": NotRequired["list[MessageTagInput]"],
# Per-email settings that override the selected route settings for this email only.
"settings": NotRequired["SendMailRequestSettings"],
"html": NotRequired[str | None],
"text": NotRequired[str | None],
"attachments": NotRequired["list[MessageAttachmentInput]"],
},
)


# Per-email settings that override the selected route settings for this email only.
SendMailRequestSettings = TypedDict(
"SendMailRequestSettings",
Expand Down
40 changes: 35 additions & 5 deletions src/lettermint/_sync/resources.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Generated from src/lettermint/_async/resources.py by scripts/unasync.py — do not edit.
"""The Team API sub-clients of :class:`~lettermint.Lettermint`.

Every method uses the team token, except ``messages.reschedule`` and
``messages.cancel``, which use the team token when it is set and otherwise
the sending token.
Every method uses the team token, except ``messages.reschedule``,
``messages.cancel`` and ``messages.resend``, which use the team token when it
is set and otherwise the sending token.
"""

from __future__ import annotations
Expand Down Expand Up @@ -48,13 +48,15 @@
MessageEventData,
MessageListData,
MessageResponse,
PendingSendMailResponse,
ProcessInboundMessageResponse,
ProjectCreatedData,
ProjectData,
ProjectListData,
ProjectMutationResponse,
ReportForwardingRequest,
RescheduleMessageRequest,
ResendMessageRequest,
ResendReportForwardingCodeResponse,
RotateProjectTokenResponse,
RouteData,
Expand Down Expand Up @@ -266,8 +268,8 @@ def update_projects(
class Messages(Resource):
"""Sent and received messages. Team token.

``reschedule`` and ``cancel`` also accept the sending token when no team
token is configured.
``reschedule``, ``cancel`` and ``resend`` also accept the sending token when
no team token is configured.
"""

def list(
Expand Down Expand Up @@ -413,6 +415,34 @@ def process(
),
)

def resend(
self,
message_id: str,
body: ResendMessageRequest | None = None,
*,
idempotency_key: str | None = None,
timeout: float | None = None,
) -> PendingSendMailResponse:
"""Sends the stored content of a sent message again, as a new message with its own ID.

Without ``body``, every original recipient receives it again; pass
``{"recipients": [...]}`` to choose some of them. The new message links to
the original through ``resent_from_message_id``. A message can be resent
while its content is retained (otherwise HTTP 410), at most 5 times in 24
hours.
"""
return cast(
PendingSendMailResponse,
self._request(
"POST /messages/{messageId}/resend",
"messages.resend",
path={"messageId": message_id},
**({} if body is None else {"body": body}),
idempotency_key=idempotency_key,
timeout=timeout,
),
)


class ReportForwarding(Resource):
"""DMARC and complaint report forwarding of a project. Team token."""
Expand Down
Loading
Loading