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),InReplyToandReferences, and a sender sends them as the mail's own headers. - The outbox is the delivery record:
Queued, thenSent(when, through which sender, the carrier's id) orHeld(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
IMailSenderoverHttp.*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
MailMessageshape — from, to, cc, subject, text, HTML, attachments, headers, message id, thread references — and matched to their conversation byInReplyTo/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.