Osy#betaRun it on Osyrin: osyrin.com

Kits · Mail

Mail

Mail an app sends — one message shape, a pluggable sender, an outbox that delivers after the commit and holds mail when there is no sender; connected mailboxes it sends from (and, with Osysharp.Mail.Receiving, reads); MIME parsing and reply extraction.

Package
Osysharp.Mail
Install
use Osysharp.Mail@0;
Group
Mail
Tests
26 in the kit’s own suite
Builds on
Identity
Used by
Accounts, People, Invitations, Mail receiving, Gmail mailbox, Microsoft 365 mailbox, IMAP mailbox, Resend mail sender, Conversations by mail, Notifications, Privacy, Privacy for received mail, Feedback — how did we do?

Mail an app sends — order confirmations, reset links, replies to a customer — through one call, whichever company carries it. The message is written in the same commit as whatever it is about, delivered after that commit, retried when the carrier is down, and held (kept, with the reason) when the app has no carrier yet — so an app works from its first minute, and nothing it meant to send is lost.

osy

SendMail(new MailMessage { To = [order.CustomerEmail], Subject = "Your order is on its way", Text = "…" }, $"shipped-{order.Id}");

Use it

osy

// app.osy
use Osysharp.Identity@0;
use Osysharp.Mail@0;
use Osysharp.Workflow;

osy

// Who may send is the app's to say — nobody else can put mail in the outbox:
partial entity OutgoingMail { security { allow create, read when IsClerk; } }

// What carries it — here Resend (the Osysharp.Mail.Resend package). No sender: mail is held.
app.Mail = new MailSetup {
  Sender = new ResendSender { ApiKey = Security.IsSecretSet("ResendApiKey") ? Secret.ResendApiKey : "" },
  From = "Acme <[email protected]>",
};

What it does

  • SendMail(message, idempotencyKey?) writes a row to the outbox (OutgoingMail) and answers it. The same key twice is one message, so a handler that runs twice mails once.
  • SendRendered(to, rendered) sends an [Email] component's answer (Osysharp.EmailTemplates).
  • ReplyTo(original, text) makes a reply in the original's conversation.
  • Threading is first-class. Every message has its MessageId (minted when the row is written, so a reply can thread to it at once), InReplyTo and References, and a sender sends them as the mail's own headers.
  • The outbox is the delivery record: Queued, then Sent (when, through which sender, the carrier's id) or Held (and why).
  • Nobody reads the outbox who was not granted — not even the recipient. A reset link sits in those rows; reading it there would skip the mailbox it was sent to.

Senders

A sender is anything implementing IMailSender — Key, IsReady(), and SendMail(message, idempotencyKey) — so the app changes carrier by changing one setting, and nothing that sends changes with it.

  • Osysharp.Mail.Resend — Resend, the transactional provider.
  • Your own — a class implementing IMailSender over Http.* for any provider, with its host declared in your package's manifest.

Reserved for a later version

The contract is shaped for what comes next, and none of it is built yet:

  • Receiving. Monitored mailboxes (Gmail and Microsoft 365, IMAP as the fallback) read into the same MailMessage shape — from, to, cc, subject, text, HTML, attachments, headers, message id, thread references — and matched to their conversation by InReplyTo/References.
  • Sending as a connected mailbox — "send as [email protected]" through the mailbox itself, as a second IMailSender.

Get it

Add the kit to your app’s manifest:

osy

app MyApp {
  use Osysharp.Mail@0;
}

Then run osy lock: it downloads the release, verifies it by its content address and pins that version in osyrin.lock. osy kits Osysharp.Mail shows what the kit is for, from inside your app.

Reference: osy docs ui-mail-kit.