Skip to content

feat!: Lettermint Rust SDK 2.0 - #12

Merged
bjarn merged 9 commits into
mainfrom
feat/v2
Oct 4, 2026
Merged

bjarn merged 9 commits into
mainfrom
feat/v2

Conversation

@bjarn

@bjarn bjarn commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A new major version, following the Lettermint SDK design agreed for all languages. The reference is the Node.js SDK 3.0 (lettermint/lettermint-node#36). Sending holds no state on the client, types are generated from the published API spec, and every SDK passes the same conformance suite.

What changes

  • One client with sendingToken and/or teamToken. A single token string is detected by prefix: lm_team_ is a team token, lm_ plus letters and digits is a sending token, and anything else is a config error. Each part uses its own token and never falls back to the other.
  • Stateless sending: emails.send(message, options), emails.sendBatch(…) and an immutable per-message compose() builder. The idempotency key is a per-call option. Attachments are objects.
  • Typed errors with the HTTP status and API error code, plus timeout, connection, unexpected-response and redirect errors.
  • Transport: redirects are never followed, so the token never reaches another host. The timeout covers the whole request, nothing is retried automatically, and tokens never appear in logs or debug output.
  • Generated types from the published API spec (lettermint/lettermint#2582, pinned to 0b4ecbdd23) using its names. Enums are open, so new API values never break decoding.
  • Team API: same layout in every SDK, with typed queries and cursor iteration.
  • Webhooks: verify(rawBody, headers) requires both X-Lettermint-Signature and X-Lettermint-Delivery, accepts any matching v1, uses a ±300 s tolerance, and keys the HMAC on the secret as-is. Reason codes are the same in every SDK.
  • UPGRADE.md has before/after for every changed call, the full type rename table, and a ready-to-copy coding-agent instruction. The previous major no longer receives updates.

Specific to this SDK

  • Includes the release fix: the crate version now comes from the tag. crates.io was stuck at 1.0.0 because Cargo.toml still said 1.0.0.
  • Every enum is open (Other(String), #[non_exhaustive]). In 1.x, a new status value turned a successful send into an error, so a retry sent the email twice.
  • Required fields are really required, and OPERATION_IDS is removed.
  • Redirect policy is none. There is a 30 s default timeout that covers the body.
  • Debug is redacted on the client, requests and webhook. Webhooks accept any v1.
  • MSRV stays 1.98.

Verification

  • 104 tests pass (unit, integration and doctests). fmt, clippy -D warnings, doc, audit and publish --dry-run pass.
  • Conformance: 42/42 with --strict. crates.io 1.0.0 fails 15.

Release

Publish a GitHub release v2.0.0. It publishes to crates.io, the first release since 1.0.0.

Docs for all SDK majors: lettermint/lettermint#2594 (stacked on lettermint/lettermint#2590). Merge it after the releases.

🤖 Generated with Claude Code

bjarn added 8 commits October 3, 2026 13:49
Releases v1.1.0 failed at "Check release version" because Cargo.toml
was never bumped by hand. Rewrite the [package] version in Cargo.toml and
Cargo.lock from the release tag before testing, packaging and publishing,
matching the Node and Python SDK release flow.
…enerator

src/generated/ is written by lettermint/sdk-generator (emit rust, next
profile, spec 80d8ab2a2d). scripts/generate.sh regenerates it,
or with --check verifies it; without the generator it checks the headers.
…d open types

One Lettermint client replaces Lettermint::email/api, EmailClient and
ApiClient. It takes a sending token and/or a team token (or one token
classified by prefix); emails() uses the sending token, the Team API the
team token, and ping/reschedule/cancel either. A missing token is a config
error that names the option.

- Types and the operation table come from the SDK generator (next
  profile): open #[non_exhaustive] enums with Other(String) that
  round-trip unknown values, required fields that are required, optional
  and nullable kept apart, CursorPage<T>, typed query structs.
- Sending: an owned builder from emails().compose(), send(options) with a
  per-call idempotency key, validation of tags before every request,
  Attachment::from_bytes/from_base64, send_batch with options.
- Team API: the Node 3.0 layout (projects().report_forwarding(),
  routes(), team().members(), webhooks().deliveries()), iterate()
  paginators that implement Stream.
- Transport: redirects are never followed (Error::Redirect), a 30 s
  default timeout that covers the body, no null body on bodiless POSTs,
  path IDs encoded and "."/".." rejected, empty/HTML/invalid bodies as
  Error::UnexpectedResponse.
- Errors: one Error enum with HTTP variants (Authentication through
  Server), status()/code(), Retry-After, Laravel field errors, tokens
  redacted from every error body including 422.
- Debug output of the client, builders, sub-clients, HttpRequest and
  Webhook redacts tokens and secrets.
- Webhooks: any v1 may match, the delivery header is required, typed
  reasons, http::HeaderMap and map headers, verify_at for replays.

BREAKING CHANGE: the 1.x entry points, endpoint structs, OPERATION_IDS,
Transport/HttpRequest shapes, error variants and many type names changed.
See UPGRADE.md.
UPGRADE.md replaces MIGRATING.md and keeps its 0.3 to 1.0 section. It
covers every changed call, the type rename table, the removed types and
exports, and states that 1.x no longer receives updates.
Test Rust 1.98 (rust-version) on Linux next to stable on every OS, run
scripts/generate.sh --check, and scan UPGRADE.md instead of MIGRATING.md.
The spec at lettermint/lettermint@0b4ecbdd23 (main, the #2582 merge) is
identical to the previous pin; only the generated file headers change.
@bjarn
bjarn requested a review from a team as a code owner October 3, 2026 22:51
async_trait marks the boxed future #[must_use], which Clippy 1.99 reports
as double_must_use. 1.x had the same scoped allow.
@bjarn
bjarn merged commit ad19c48 into main Oct 4, 2026
7 checks passed
@bjarn
bjarn deleted the feat/v2 branch October 4, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants