Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Mail on the wire — Mime.Parse, Mime.Build, Imap.Fetch, Smtp.Send

string Mime.Parse(byte[] raw) · byte[] Mime.Build(string json) · string Imap.Fetch(host, port, username, credential, oauth, folder, cursor, max) · string Smtp.Send(host, port, username, credential, oauth, message)

The four calls a mail kit is built from. `Mime.Parse` reads a whole RFC 5322 message — headers, text and HTML bodies, attachments, and a bounce's failed Message-IDs — into JSON in the shape of `Osysharp.Mail`'s `MailMessage`; `Mime.Build` writes that shape back as bytes. `Imap.Fetch` and `Smtp.Send` read and send through any mail host over the platform's guarded transport. An app reaches them through `Osysharp.Mail` and its mailbox kits, rarely directly.

preview2 examples compiled by CImailstdlibintegration

Summary#

A mail message is BYTES — RFC 5322 text with MIME parts inside. These calls turn those bytes into data an app can read, data back into bytes a provider sends, and move them to and from a mail host:

MailMessage mail = JsonSerializer.Deserialize<MailMessage>(Mime.Parse(raw));   // what Osysharp.Mail's ParseMail does
byte[] raw = Mime.Build(JsonSerializer.Serialize(mail));                         // …and RawOf

Most apps never write them: Osysharp.Mail wraps the first two as ParseMail / RawOf, and Osysharp.Mail.Imap's ImapMailbox is built on the other two. They are here for the kit you write next.

Signature#

string Mime.Parse(byte[] raw)    // → JSON: From, To, Cc, Bcc, ReplyTo, Subject, Date, MessageId, InReplyTo,
                                 //   References, Text, Html, Headers, Attachments[{FileName, Content (base64),
                                 //   ContentType, ContentId}], FailedMessageIds (a bounce report only)
byte[] Mime.Build(string json)   // the same shape (any letter case) → RFC 5322 bytes

// `use Osysharp.Http;` — outbound network, so the same opt-in as Http.*
string Imap.Fetch(string host, int port, string username, string credential, bool oauth,
                  string folder, string cursor, int max)
string Smtp.Send(string host, int port, string username, string credential, bool oauth, byte[] message)

Description#

Parse reads everything a mail carries#

Mime.Parse answers the message in MailMessage's own shape, so JsonSerializer.Deserialize<MailMessage> is the whole job. The bodies come both ways a client sends them (Text, Html); Headers keeps every header's first value; each attachment carries its bytes as base64 and its ContentId when the HTML places it inline. Date is null when the message has none — never "now", which would be a fact the sender did not state.

A bounce says which message failed. A delivery-status report (RFC 3464) names the message it returns in its own attached part, which is the one place a bounce reliably carries it — many carry no In-Reply-To. FailedMessageIds holds those ids, for a FAILURE report only, so a receiver can mark exactly that delivery failed.

Build writes what a provider sends#

Mime.Build takes the same JSON and answers the bytes: multipart when there are attachments or both bodies, every header the message carries, its Message-ID and threading headers as given. It stamps no Date of its own — a build is deterministic, so the same message builds to the same bytes, and a durable retry sends what the first try would have.

Imap.Fetch and Smtp.Send move bytes, guarded#

Both run over the host's mail transport, which refuses a private or loopback address exactly as Http.* does, requires TLS (993/465) or upgrades with STARTTLS, and never logs a credential. The credential is an app password, or — with oauth: true — an OAuth access token (XOAUTH2); either way it is a secret sink: pass a Secret.X or a setting filled from one, never a literal. A host that refuses the login, is down or is not allowed is an ExternalServiceException naming the host, never the credential.

  • Imap.Fetch answers JSON { "Cursor": "…", "Notice": "…", "Messages": [{ "Uid": 42, "Raw": "<base64>" }] }: the messages after cursor in folder, oldest first, at most max, read with PEEK (nothing is marked read). An EMPTY cursor answers no mail and the cursor of NOW — a mailbox connected today does not import its history. A cursor carries the folder's UIDVALIDITY; when the server re-creates the folder, the fetch starts again from now and Notice says so in a sentence an admin can read.
  • Smtp.Send submits the bytes as they are — their From, recipients and Message-ID — and answers the server's reply.

A test stubs either like any effect: Imap.Fetch.Stub((host, port, user, credential, oauth, folder, cursor, max) => …).

Examples#

Read a received message's subject and whether it is a bounce:

class Seen {
  public string Subject = "";
  public List<string> FailedMessageIds = [];
}

string SubjectOf(byte[] raw) {
  return JsonSerializer.Deserialize<Seen>(Mime.Parse(raw)).Subject;
}

bool IsBounce(byte[] raw) {
  return JsonSerializer.Deserialize<Seen>(Mime.Parse(raw)).FailedMessageIds.Count > 0;
}

Build a plain message's bytes:

class Outgoing {
  public string From = "";
  public List<string> To = [];
  public string Subject = "";
  public string Text = "";
}

byte[] Hello(string to) {
  return Mime.Build(JsonSerializer.Serialize(new Outgoing {
    From = "Acme <[email protected]>", To = [to], Subject = "Hello", Text = "Welcome aboard.",
  }));
}

See also#

Related

The Mail kit — send a message, keep a record, pick a sender

`Osysharp.Mail` is the contract between an app that sends mail and whatever carries it. `SendMail` writes the message…

Receiving mail — watched mailboxes (Gmail, Microsoft 365, IMAP)

An app WATCHES a mailbox — it is never a mail server. The mail arrives where it always did (a Google Workspace or…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…

Encoding — Base64, hex, text encodings, URL, HTML

Encode and decode text — Base64, URL percent-encoding, and HTML escaping — with the C#-faithful spellings…