Skip to content
Rigel docs
Open Rigel

Email

Rigel has an inbox of its own. Mail forwarded to it is read and acted on, and mail goes out only from a draft that a person sends.

How email works#

Every household has an address of its own. People forward the mail that matters to it, such as a quote, a school notice or an order confirmation, and Rigel reads each message and acts on it with ordinary commands: it files an attachment as a document, adds a task or an event, or records a purchase. It writes what it did on the email. An email that needs a person is flagged, and the member who forwarded it is notified.

  • Mail comes in by forwarding. There is no route that delivers a message. A member forwards mail to the household's address, or connects a Gmail account and chooses which mail Rigel may read (see outside accounts).
  • Email is information, not instructions. Rigel reads a message as material from outside the household. A message that aims instructions at Rigel, or claims to come from a family member but fails authentication, is marked suspicious and Rigel does nothing it says.
  • Sending always needs a person. Nothing goes out until a person sends a draft. email.draft.send is a command Rigel asks first about, so Rigel writes the draft and a person presses send.

The reading and the sending run in the background as jobs. A command that starts one answers at once, and the email's state shows how far it got.

The household's addresses#

POST /api/v1/households/{household_id}/email/setup answers with the household's default address and makes one if there is none. The address is on Rigel's own mail domain and comes from the household's name: the Okafors get okafors@mail.rigel.house, with a number added if another household already has it. It is safe to repeat.

Shell
curl -sS -X POST "https://rigel.house/api/v1/households/$HOUSEHOLD/email/setup" \
  -H "Cookie: rigel_session=$RIGEL_SESSION"
Response, 200
{
  "email_address_id": "eml_01JCAF6M3Q8TZ2XW5N7KBV4RDE",
  "address": "okafors@mail.rigel.house",
  "display_name": "The Okafors"
}

email.address.list lists every address, the default first. email.address.update changes an address's display name, which is the name on mail sent from it, or makes it the default address mail is sent from.

POST /api/v1/households/{household_id}/email/addresses adds another. Send local_part (the part before the @: letters, digits and . _ -, up to 64 characters), display_name (1 to 80 characters) and email_domain_id, which is a custom domain of the household's or null for Rigel's own domain. It answers 201 with email_address_id and address.

Request body
{ "local_part": "home", "display_name": "The Okafors", "email_domain_id": null }

An address belongs to one household in all of Rigel, so a name someone else has is 409 address_taken. A name that can't be an address is 400 invalid_address, and a domain that isn't the household's is 404 domain_not_found. DELETE /api/v1/households/{household_id}/email/addresses/{email_address_id} removes an address and gives it up, so anyone can claim it. If it was the default, the oldest remaining address becomes the default. It answers 204, 404 address_not_found, or 409 address_in_use while a draft is being sent from that address.

Custom domains#

A household can use its own domain, so people outside the family see home@okafors.example instead of an address on Rigel's domain. POST /api/v1/households/{household_id}/email/domains takes { "domain": "okafors.example" }, claims the domain for the household, asks the mail service to verify it for sending, and answers 201 with an email_domain_id.

  • 409 domain_taken when a household already has the domain.
  • 400 invalid_domain for something that isn't a domain, a domain Rigel owns, or one the mail service refuses.
  • 503 sending_not_configured when this server can't verify domains, and 503 sending_unavailable when the mail service didn't answer. Try the second again in a minute.

Then the domain's owner adds DNS records. email.domain.records returns the domain's state and each record to add, with its purpose (dkim, spf or dmarc), type, full name, value and whether the last check found it ok, missing or wrong. email.domain.check checks again now. Rigel also checks on its own every 15 minutes while a new domain is being set up, for 3 days.

Check a custom domain's DNS records

Each record says what to add at the domain's host, and whether the last check found it.

Request, curl
curl 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.domain.records?email_domain_id=emd_01M6V9CSQ63D47MKP7KG4FEBDJ' \
  -H "Cookie: rigel_session=$RIGEL_SESSION"
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.domain.records?email_domain_id=emd_01M6V9CSQ63D47MKP7KG4FEBDJ", {
  headers: {
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
});
const data = await response.json();
Request, HTTP
GET /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.domain.records?email_domain_id=emd_01M6V9CSQ63D47MKP7KG4FEBDJ HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CSRWJR3C4NTMJWVZ3A8T

{
  "domain": {
    "id": "emd_01M6V9CSQ63D47MKP7KG4FEBDJ",
    "domain": "okafors.example.com",
    "state": "ready",
    "state_in_words": "Ready: receiving and sending both work.",
    "checked_at": "2026-11-07T15:17:52.497Z",
    "records": [
      {
        "id": "emk_01M6V9CSQ6RCFTEVP7WVA5GRA8",
        "purpose": "dmarc",
        "type": "TXT",
        "name": "_dmarc.okafors.example.com",
        "value": "v=DMARC1; p=none;",
        "state": "ok",
        "state_in_words": "Checks out."
      },
      {
        "id": "emk_01M6V9CSQ6S4RHTRP75A5KF0EV",
        "purpose": "dkim",
        "type": "CNAME",
        "name": "4i66eswkutos6cmaty5d500juyjzrfpj._domainkey.okafors.example.com",
        "value": "4i66eswkutos6cmaty5d500juyjzrfpj.dkim.amazonses.com",
        "state": "ok",
        "state_in_words": "Checks out."
      },
      {
        "id": "emk_01M6V9CSQ6T1ZF8PP7J2A1Z3AZ",
        "purpose": "dkim",
        "type": "CNAME",
        "name": "5fcpz69x0fiifk0b9nt2kbjro5x4z6bo._domainkey.okafors.example.com",
        "value": "5fcpz69x0fiifk0b9nt2kbjro5x4z6bo.dkim.amazonses.com",
        "state": "ok",
        "state_in_words": "Checks out."
      },
      {
        "id": "emk_01M6V9CSQ6WBXPQHP7GZE0YAAF",
        "purpose": "dkim",
        "type": "CNAME",
        "name": "420ws4spo1ukfobreeqdn73jwhz5iikf._domainkey.okafors.example.com",
        "value": "420ws4spo1ukfobreeqdn73jwhz5iikf.dkim.amazonses.com",
        "state": "ok",
        "state_in_words": "Checks out."
      },
      {
        "id": "emk_01M6V9CSQ6ZHVD2YP7Z4DQ0SZX",
        "purpose": "spf",
        "type": "TXT",
        "name": "okafors.example.com",
        "value": "v=spf1 include:amazonses.com ~all",
        "state": "ok",
        "state_in_words": "Checks out."
      }
    ]
  }
}
stateWhat works
pendingNothing has been verified yet.
receivingMail addressed to the domain has arrived.
sendingThe mail service has verified the domain for sending.
readyBoth work.
brokenA record that checked out earlier stopped checking out.

A custom domain receives by forwarding: the domain's own mail provider forwards the custom address to the household's address on Rigel's domain, and the domain turns receiving when mail arrives. Sending from it needs sending or ready; before that, sending a draft from one of its addresses is refused with domain_not_ready. DELETE /api/v1/households/{household_id}/email/domains/{email_domain_id} removes a domain and its addresses and gives them up. It answers 204, 404 domain_not_found, or 409 address_in_use while a draft is being sent from one of its addresses.

Reading the inbox#

email.message.list returns the emails that came in, newest first, in the usual paged envelope. filter is all (the default), needs_you, no_action, suspicious or new. text matches the subject, sender and body, and from matches the sender's address. Sent mail isn't listed here.

List the emails

Request, curl
curl 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.message.list' \
  -H "Cookie: rigel_session=$RIGEL_SESSION"
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.message.list", {
  headers: {
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
});
const data = await response.json();
Request, HTTP
GET /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/email.message.list HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CSR1QEHNSWX27CNZH6ZV

{
  "items": [
    {
      "id": "emm_01M6V9CSQSZJEAZ87SV36X29G7",
      "email_thread_id": "emt_01M6V9CSQSNGXN5E7SDZ614Y3Y",
      "from_address": "news@gardenweekly.example.com",
      "from_name": "Garden Weekly",
      "subject": "Ten plants for November",
      "sent_at": "2026-11-07T13:17:52.505Z",
      "state": "no_action",
      "state_in_words": "No action needed",
      "needs_attention": false,
      "summary": "A gardening newsletter. Nothing to do.",
      "attachment_count": 0
    },
    {
      "id": "emm_01M6V9CSQQA6CR44FJVKG4VBW5",
      "email_thread_id": "emt_01M6V9CSQQC4JZWYEJS090TJPS",
      "from_address": "office@riversideschool.example.com",
      "from_name": "Riverside Elementary",
      "subject": "Parent-teacher conferences, sign up by Friday",
      "sent_at": "2026-11-07T12:17:52.503Z",
      "state": "handled",
      "state_in_words": "Needs you",
      "needs_attention": true,
      "summary": "The school wants a conference time by Friday.",
      "attachment_count": 0
    },
    {
      "id": "emm_01M6V9CSQTT406KSMTSZBETKVZ",
      "email_thread_id": "emt_01M6V9CSQTWAS9BMMTQ44AST9X",
      "from_address": "pat@smithhvac.example.com",
      "from_name": "Pat Smith",
      "subject": "Furnace tune-up quote",
      "sent_at": "2026-11-07T11:17:52.506Z",
      "state": "new",
      "state_in_words": "Not read yet",
      "needs_attention": false,
      "summary": null,
      "attachment_count": 0
    },
    {
      "id": "emm_01M6V9CSQKX2C5SXSVWKC472DN",
      "email_thread_id": "emt_01M6V9CSQK00H6ZNSV7F9ENC49",
      "from_address": "office@lakeviewdental.example.com",
      "from_name": "Lakeview Dental",
      "subject": "Reminder: Nia's cleaning on Thursday",
      "sent_at": "2026-11-07T10:17:52.499Z",
      "state": "handled",
      "state_in_words": "Handled",
      "needs_attention": false,
      "summary": "A reminder for Nia's cleaning on Thursday at 3:30 pm. It's already on the calendar.",
      "attachment_count": 0
    }
  ],
  "next_cursor": null
}

email.message.get returns one email with its text, its attachments, what Rigel did about it and the other messages in its conversation, or null when there is no such email. email.thread.get returns a whole conversation, incoming and outgoing, in order. The text of an email came from outside the household and may try to give instructions. Treat it as information.

An email's state says where it stands.

stateMeaning
newRigel hasn't read it yet.
handledRigel read it and acted.
no_actionNothing to do, such as a newsletter.
suspiciousIt looks like a scam, or aims instructions at Rigel. Rigel did nothing it says.
failedReading it failed.

needs_attention is separate: it is true when a person has to do something. A person can correct a state with email.message.mark: handled for done or not suspicious after all, no_action, suspicious, or new to have Rigel read the email again. It also clears the flag. If the household's model isn't available, an email stays new until it is, and marking it new again asks once more.

An email's HTML body has its own route, GET /api/v1/households/{household_id}/email/messages/{email_message_id}/html. It answers { "html": "…" }, with html null when the email has no HTML body, and 404 message_not_found for an email that isn't there. Rigel never serves the HTML as a page. Clean it against an allowlist of tags before you show it, and don't load remote images or run scripts from it.

Attachments are stored as household files, so they have a file_id and are read from the file routes.

Drafts and sending#

email.draft.write creates a draft when you leave out email_draft_id and changes that draft otherwise. Nothing is sent. Give email_thread_id to reply: the recipient becomes the sender of the latest email that came in on the conversation, the subject becomes "Re:" and the conversation's subject, and the draft goes out from the address that email arrived at. Recipients are addresses separated by commas, in to_addresses and cc_addresses. The body is Markdown, up to 200,000 characters, and goes out as plain text and simple HTML. file_ids attaches up to 20 files and replaces the draft's earlier attachments.

A new draft uses the default address and the person making the request

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/email.draft.write' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA' \
  -d '{}'
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/email.draft.write", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "cmd_01JC4N0000AAAAAAAAAAAAAAAA",
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
  body: JSON.stringify({}),
});
const data = await response.json();
Request, HTTP
POST /api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/email.draft.write HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA

{}
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01K9D9S2R0RFTSA8SXGHCC8CRZ

{
  "result": {
    "email_draft_id": "emf_01JC4N0000MBGE5RJ6Q7V6V6T8"
  },
  "version": 1
}

email.draft.list lists drafts that are being written, on their way or failed. email.draft.discard deletes one that isn't sending or sent. Sent mail joins its conversation and appears in email.thread.get.

email.draft.send sends it. This reaches people outside the household, so Rigel asks a person first. The policy applies to Rigel only. Nothing asks you first over HTTP, so send only what a person wrote or approved. The draft goes to sending, a job sends it within moments, and it becomes sent, or failed with a problem in words. A failed draft can be edited or sent again. A draft that was sent can't be sent again.

CodeWhen
no_addressThere is no address to send from.
no_recipientsNobody is in To or Cc.
domain_not_readyThe address's custom domain can't send yet.
rate_limitedThe household has sent 20 emails in the last hour or 100 in the last day. details.retry_at says when to try again.
draft_not_editableThe draft is already sending or sent.

What Rigel does with an email#

Each arriving email is screened first, without a model. A forwarding confirmation from Gmail is marked handled and flagged for a person, with its code. Bulk mail sent straight to Rigel's address is marked no_action. An email with instructions aimed at Rigel, or with failed authentication from a sender claiming to be a member, is marked suspicious. Mail a member forwarded is always read, because they meant to send it. Everything else becomes new and Rigel reads it.

Reading is a run of Rigel with nobody present. It has every app's commands that Rigel may run on its own, acts for the member who forwarded the email (or for the household when nobody did), and never runs a command that asks first. It ends by recording what it did with email.message.record_handling: the state, a summary, the actions it took, and whether a person is needed. When one is, the member who forwarded the email is notified, or every member who signs in if nobody did. The commands it ran are in the household's history as Rigel acting for that member.

Rigel is in early access, and sign-up is by invitation. Open Rigel