# Kuju Email — agent runbooks (full corpus) > Static, read-only instructions a customer's AI agent can follow to walk them through Kuju Email invite redemption, DNS delegation, mailbox migration and delivery troubleshooting. Every command is read-only. Angle-bracket placeholders like are for you to fill in at run time. Steps marked HUMAN ACTION cannot be done by an agent: hand them to the person and wait. # Start here You are an agent helping a person set up or run Kuju Email. This corpus is a set of runbooks written for you, not for them — read this page fully before opening any other. Never invent a value you did not observe. Every command anywhere in this corpus is read-only. ## The rules 1. **Only run the commands the runbooks show.** They are all read-only (`dig`, `nslookup`, `curl -sI`, `openssl s_client`). Do not improvise commands that write, log in, or change anything on any system. A 4-space-indented block is not always something to run - it can also be words to say to the person (a script, for example migration's cap explanation) or a template for them to fill in (for example the support report). The sentence introducing it says what it is; only a command is something you execute. 2. **Branch on observations, not on guesses.** Every step has a table of what you might observe and what to do next. If your observation is not in the table, stop and tell the person exactly what you saw. 3. **Values come in three kinds.** `` are yours to fill in at run time - `` is the person's domain, `` is a value they read to you. A value with no brackets is real and current; do not "correct" it. A third kind is an illustrative example, always marked locally where it appears: an invite that "looks like `KUJU-7F3K-9QM2`", a worked nameserver table using `ns-1234.awsdns-56.org`, or a migration script sized in GB and months. None of these is the person's value - never report one as something you observed. 4. **HUMAN ACTION means you cannot do it.** A step tagged **HUMAN ACTION** is off-limits to you — for example a registrar login, a control panel, a password, or a screen inside Kuju. Give the person precise instructions and wait. Never claim you completed a HUMAN ACTION step. 5. **Never handle secrets.** Do not ask for passwords, invite codes or app passwords, and decline them if offered. 6. **Report what you observed, not what you concluded.** Paste command output when you report; say MISSING when something is missing. ## Before you start Check which tools you have: dig -v curl --version openssl version | Observation | Next | | --- | --- | | all three print a version | continue | | `dig` is missing | use `nslookup -type= ` wherever a runbook shows `dig `. For NS and MX, read the `nameserver =` / `mail exchanger =` lines when present. `nslookup` prints "can't find" for BOTH an absent name and a present name with no record of that type - never key off that phrase; read what follows the colon instead: `: NXDOMAIN` means the name does not exist, `: No answer` means the name exists but has no record of that type. For TXT and SRV, a present record is prefixed with a `text =` / `service =` label, so compare the value on the right of `=`, not the whole line. `nslookup` CAN query a specific nameserver — pass it as a trailing argument rather than `@server` — but it never prints a `status:` line, so any step that branches on `status:` cannot be read through it: tell the person which command you cannot run, and ask them to run it and report back what it printed, the same as when `curl` or `openssl` is missing | | `curl` or `openssl` is missing | tell the person which command is missing; read them the step's command and ask them to run it and report back what it printed | ## Which runbook | The person wants to | Open | | --- | --- | | redeem an invite they received (Kuju is invite-only during the beta) | [Redeem a Kuju Email invite](https://www.kaimoku.tech/kuju-email/agent/signup-trial.md) | | point their own domain at Kuju (needs an active Kuju account — [redeem the invite](https://www.kaimoku.tech/kuju-email/agent/signup-trial.md) first) | [Set up your domain's DNS for Kuju](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) | | bring their existing mail across from another provider | [Move an existing mailbox into Kuju](https://www.kaimoku.tech/kuju-email/agent/migration.md) | | find out why mail is not arriving, bounces, or lands in spam | [Troubleshoot mail delivery for a Kuju domain](https://www.kaimoku.tech/kuju-email/agent/troubleshooting-delivery.md) | | understand a sending-limit, rate-limit or quota error they hit while sending | [Troubleshoot mail delivery for a Kuju domain](https://www.kaimoku.tech/kuju-email/agent/troubleshooting-delivery.md), section C | Reference, when a term is unfamiliar: the [glossary](https://www.kaimoku.tech/kuju-email/glossary.md). The [API reference](https://www.kaimoku.tech/kuju-email/docs.md) exists for completeness; no runbook needs it and you should not call the API. ## Where these files come from Every runbook is generated from a single source of facts at build time, so the values you see reflect the latest deploy. If a value in a runbook disagrees with what you observe, trust your observation, tell the person, and continue with the runbook's "if not" branch. --- # Redeem a Kuju Email invite **Before you start.** This runbook assumes: - the customer has received an invite from a Kuju Site Admin (an email containing a link and a code) - you can run curl If one of these is not true, say so before you continue — a step below may already tell you what to do about it, so keep reading before you stop. Only abort here if nothing further down handles it. You are an agent helping a person redeem a Kuju Email invite and start using it. Kuju Email is invite-only: there is no public signup, and a Kuju Site Admin issues an invite to a named person only. There are two kinds of invite, each redeemed on its own page: a **trial invite** (a time-limited trial mailbox on Kuju's demo domain) and a **beta mailbox invite** (a beta mailbox, on the demo domain or the person's own). If the person has no invite, stop after Step 1. Never invent a value you did not observe. Every command here is read-only. Steps marked **HUMAN ACTION** are things you cannot do — for example opening the invite email, clicking the redemption link or typing the code, entering a password, contacting the Site Admin who sent the invite, or answering a question only they can answer (such as which domain they own). Give the person exact instructions, then wait for them to confirm. ## Step 1 - Find the invite > **HUMAN ACTION** - ask the person to open the invite email — it carries > one secret, rendered two ways: a clickable link, and the same token as a > human-typeable code that looks like `KUJU-7F3K-9QM2`. They are not two > different invites - either one redeems it, and using one does not > invalidate the other until the invite is used or expires. | Observation | Next | | --- | --- | | the person has the email with a link | Step 2, using the link — it already opens the right page for its kind | | the person has only the code (for example read out over the phone) | Step 2, using the code — ask whether the email offered a **trial** or a **beta mailbox**; that decides the page | | the person has neither | there is nothing to redeem; **HUMAN ACTION** - tell them to ask whoever offered them access to have a Kuju Site Admin send an invite, then stop | | the link says the invite is expired or already used | **HUMAN ACTION** - ask the Site Admin to re-send it; a re-send issues a fresh secret | Never ask the person to paste the link or the code to you. You do not need it. ## Step 2 - Check the redemption page is open The page depends on the invite's kind: | Invite | Page | | --- | --- | | trial invite | `https://mail.kuju.email/signup` | | beta mailbox invite | `https://mail.kuju.email/join` | Before sending the person there, observe whether that page is accepting invites (shown for the trial page; use the beta page's address for a beta mailbox invite): curl -sI https://mail.kuju.email/signup Read the first line (the status) and the `location:` header if present. | Observation | Meaning | Next | | --- | --- | --- | | `HTTP/2 200` (or `HTTP/1.1 200`) | the page is open | Step 3 | | `303` or `302` with `location:` ending in `/login` | the beta gate is not open yet on this server; invites cannot be redeemed today | **HUMAN ACTION** - tell the person to reply to whoever sent the invite and ask when redemption opens; stop | | connection error or `5xx` | the service is unreachable right now | wait 15 minutes and re-run; if it is still failing, **HUMAN ACTION** - report it to whoever sent the invite | ## Step 3 - Redeem > **HUMAN ACTION** - the person clicks the link, or opens the page from Step 2 > and types the code. They set their own name and password there. You cannot > do this for them and must not see the password. If the page answers "This invite is for a different signup.", the code is real but belongs to the other page: send them to the other address in Step 2. | Invite | After redemption | Next | | --- | --- | --- | | trial invite | a mailbox on the demo domain, working immediately, for the trial period the page states; there is no choice screen | [migration](https://www.kaimoku.tech/kuju-email/agent/migration.md) if they want their old mail in it; otherwise done | | beta mailbox invite | the choice screen in Step 4 | Step 4 | ## Step 4 - Choose the path (beta mailbox invite only) After redeeming a beta mailbox invite, Kuju asks how the person wants to use the beta. Both paths are real and both are worth exercising; help them choose by asking one question: > **HUMAN ACTION** - ask: "Do you own a domain you want to receive mail on > right now, or do you want to try Kuju first?" | Answer | Choice on screen | What it gives them | Next | | --- | --- | --- | --- | | "try it first" | a mailbox on the demo domain | an address ending in Kuju's demo domain, working immediately, nothing to configure | [migration](https://www.kaimoku.tech/kuju-email/agent/migration.md) if they want their old mail in it; otherwise done | | "my own domain" | **bring your own domain** | Kuju's domain wizard: enter the domain, pick a DNS strategy, verify | [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) | The choice is not final: a demo-domain tester can add their own domain later. ## Step 5 - What not to say The invite does not carry a plan or a price, and you must not infer one. If the person asks which plan they are on, what it will cost, or how long their access lasts, tell them: the invite grants access, not a plan; plan, pricing and terms are settled separately with Kaimoku, and whoever sent the invite is the one to ask. The one exception is a trial invite's length: you may repeat the period the trial page itself states. Do not guess any other answer from anything you have seen elsewhere. ## Step 6 - Report Tell the person: whether the redemption page was open (Step 2 status), which kind of invite they redeemed (and, for a beta mailbox invite, which path they chose), and the name of the next runbook. --- # Set up your domain's DNS for Kuju **Before you start.** This runbook assumes: - the customer owns a domain and can log in to wherever it is registered - the customer has an active Kuju account (see signup-trial) — redeem the invite first; do not start this runbook without one - you can run dig (or nslookup) If one of these is not true, say so before you continue — a step below may already tell you what to do about it, so keep reading before you stop. Only abort here if nothing further down handles it. You are an agent helping a person set up Kuju Email for ``: either by moving the domain's DNS to Kuju, or by adding Kuju's mail records to the DNS host they already use. Replace `` with the customer's domain everywhere below (for example `example.com`). Never invent a value you did not observe. Every command here is read-only. Steps marked **HUMAN ACTION** are things you cannot do — for example registrar logins, clicking through control panels, reading a value from the Kuju admin screen. Give the person exact instructions, then wait for them to confirm. ## Step 1 - Find out who runs this domain's DNS today dig NS Read the `status:` in the header and the `ANSWER SECTION`. | Observation | Next | | --- | --- | | `status: NXDOMAIN` | Step 1a - the name does not exist | | `status: NOERROR` and the ANSWER SECTION is empty | Step 1b - a subdomain hosted inside its parent zone | | every NS answer ends in `.kuju.email.` | Step 5 - already delegated, so Kuju publishes the mail records; verify only | | NS answers present, any other host | Step 2 | | `status: SERVFAIL`, `status: REFUSED`, or no response (timeout) | retry the lookup once; if it still fails, stop and tell the person the lookup itself is failing — do not guess a registrar from absent data | If `dig` is not installed, `nslookup -type=NS ` gives the same answer, but it prints "can't find" for BOTH outcomes below - never key off that phrase; read what follows the colon instead: `: NXDOMAIN` is Step 1a (the name does not exist), `: No answer` is Step 1b (no NS published here); otherwise read the `nameserver =` lines. Write the NS answers down. They name the host that answers for this domain right now, which Step 3b needs in order to query it directly, and Step 6 reports them on every path. ### Step 1a - The name does not exist > **HUMAN ACTION** - the domain is not registered, or was typed wrong. Ask the > person to confirm the spelling. If it is correct, they need to register the > domain first; come back to Step 1 afterwards. ### Step 1b - A subdomain hosted inside its parent zone `` is a subdomain (such as `mail.example.com`) whose records live in the parent domain's DNS (`example.com` in that example). **Do not follow Step 3a's script on this path.** "Set custom nameservers" is a registrar-level control that replaces the PARENT domain's own top-level NS records — applying it here would delegate all of `example.com`, not just the subdomain, and would very likely take the parent domain's entire DNS offline. That is worse than the delegation never happening, so treat this branch as its own procedure: 1. Repeat Step 1 with the parent domain to see who hosts ITS DNS. 2. Identify that host with Step 2's substring table — matching logic only; this path does not continue to Step 3. 3. Then use this script: > **HUMAN ACTION** - you cannot do this step. Give the person: > > 1. The instruction to add `` in the Kuju admin, choose > **Use Kuju DNS**, and provision it. The choice > matters: the wizard's other option is pre-selected, and picking it would > leave Kuju with no zone for the subdomain, so the delegation below could > never work. > 2. The DNS panel link or host name from Step 2 (or the nameserver hostnames > you observed if no key matched). > 3. The instruction to open the parent zone's DNS RECORDS section — not the > "nameservers" / "custom nameservers" setting, which controls the parent > domain's own top-level NS and must be left exactly as it is — and add two > NS records scoped to the subdomain label only: > > Add NS records for `` pointing at: > ns1.kuju.email > ns2.kuju.email > > Most panels want just the label (`mail`), not the full `mail.example.com`, > and both records must be saved, not one. > 4. An explicit statement, in your own words: this delegates only ``; > the parent domain's own nameserver setting must not change. > > Ask them to tell you when they have saved the change. Continue at Step 4, running the same `dig NS ` checks against the subdomain and using its "from Step 1b" row. ## Step 2 - Identify the registrar or DNS host Take the FIRST line of the NS answers from Step 1. Lowercase it and strip the trailing dot. Then test whether that hostname CONTAINS each key in the table below - a substring test, not a suffix test. Two of the biggest providers put their key in the MIDDLE of the name - illustrative examples, not something you observed: ns-1234.awsdns-56.org contains "awsdns" -> AWS Route 53 ns1-08.azure-dns.com contains "azure-dns" -> Microsoft Azure dns1.registrar-servers.com contains "registrar-servers.com" -> Namecheap | If the first nameserver CONTAINS | Registrar / DNS host | DNS panel | | --- | --- | --- | | `registrar-servers.com` | Namecheap | https://ap.www.namecheap.com/domains/domaincontrolpanel//advancedns | | `domaincontrol.com` | GoDaddy | https://dcc.godaddy.com/dns/ | | `cloudflare.com` | Cloudflare | https://dash.cloudflare.com | | `google.com` | Google / Squarespace | https://domains.squarespace.com | | `googledomains.com` | Google / Squarespace | https://domains.squarespace.com | | `awsdns` | AWS Route 53 | https://console.aws.amazon.com/route53 | | `azure-dns` | Microsoft Azure | https://portal.azure.com/#browse/Microsoft.Network%2FdnsZones | | `digitalocean.com` | DigitalOcean | https://cloud.digitalocean.com/networking/domains/ | | `linode.com` | Linode / Akamai | https://cloud.linode.com/domains | | `hover.com` | Hover | https://www.hover.com/control_panel/domain/ | | `name-services.com` | Enom / Tucows | no panel link — name the registrar and hand off (HUMAN ACTION) | This step finds out WHERE the person will be working. It does not decide what they change there. Every row below continues to Step 3; they differ only in what you carry with you. | Observation | Carry into Step 3 | | --- | --- | | exactly one key matches and it has a panel link | that link, with `` filled in | | the key matches but the row says "no panel link" | the host's NAME; the person will have to find the right screen there themselves | | no key matches | the nameserver hostnames you saw | ## Step 3 - Choose how the domain will use Kuju There are two ways to finish this, and the choice belongs to the person, not to you. Kuju's domain wizard asks the same question — "Use Kuju DNS" or "Keep your current DNS" — so whatever they answer here is what they must pick there too. Two facts you will need on either path: - **An SPF record is a TXT record whose value begins `v=spf1`.** Every other TXT record a domain carries — `google-site-verification=`, `MS=`, and anything else — is not a mail record and is none of Kuju's business. - **Kuju's MX and SPF records REPLACE the old provider's; they never join them.** A second MX record means mail can keep flowing to the old host — and if the old MX has a lower preference number than Kuju's `10`, essentially all of it will. A second `v=spf1` record makes SPF fail outright for every message, a PermError per RFC 7208, so neither record is used. > **HUMAN ACTION** - you cannot choose for them. Ask: "Do you want Kuju to run > all of this domain's DNS, or do you want to keep your current DNS host and > just add Kuju's mail records to it?" Give them both consequences before they > answer: > > - **Kuju runs all DNS.** Nothing to maintain afterwards; Kuju publishes the > mail records and keeps them correct. But every record the domain has today — > website, subdomains, domain-verification strings, anything — has to be > recreated on Kuju's side or it stops resolving. > - **Keep the current DNS host.** Nothing else about the domain moves. But they > own the four mail records, and if Kuju ever issues a new one (a new DKIM > key, say) they have to copy it into their DNS by hand. | Answer | Next | | --- | --- | | Kuju runs all DNS ("Use Kuju DNS" in the wizard) | Step 3a | | keep the current DNS host ("Keep your current DNS" in the wizard) | Step 3b | ### Step 3a - Move all DNS to Kuju Collect what exists now, so the person can recreate it on Kuju's side: dig A +short dig AAAA +short dig A www. +short dig CNAME www. +short dig MX +short dig TXT +short Keep every non-empty answer; Step 6 reports them. When `www` is a CNAME, the `A` query prints the CNAME target as well as the addresses — the separate `CNAME` query tells you which value that is. Then: > **HUMAN ACTION** - you cannot do this step. Give the person: > > 1. The instruction to add the domain in the Kuju admin, choose > **Use Kuju DNS**, and provision it. This has to > happen BEFORE the nameservers change, so that Kuju's nameservers have a > zone to answer for, and the choice matters: the wizard's other option is > pre-selected. > 2. The DNS panel link or host name you carried from Step 2 (or the nameserver > hostnames you observed in Step 1, if no key matched — that is the only > identifying information you have). > 3. The captured A, AAAA and CNAME lines, plus any TXT line that does NOT begin > `v=spf1`, with the instruction to add them in Kuju's domain DNS page after > delegation. Do NOT forward the captured MX line or any `v=spf1` TXT line: > Kuju publishes its own mail records once delegated, and re-adding the old > values alongside them is the failure described in Step 3. Hold those two > values for your Step 6 report instead. > 4. The exact two nameservers to set, replacing whatever is there now: > > Set custom nameservers to: > ns1.kuju.email > ns2.kuju.email > > 5. A warning to watch for a "confirm this change" email from the registrar and > click it. An unclicked confirmation is the single most common reason a > correctly-entered nameserver change never takes effect. > > Ask them to tell you when they have saved the change. Registrars usually > apply it within minutes; some take up to 48 hours. Continue at Step 4. ### Step 3b - Keep the current DNS host and add Kuju's records Nothing moves. The nameservers stay exactly as Step 1 found them, and the person adds Kuju's four mail records where their DNS already lives. First, find out whether the domain already publishes mail records that would collide with Kuju's: dig MX +short dig TXT +short | Observation | Next | | --- | --- | | MX is already `10 mail.kuju.email.` AND a `v=spf1` record equal to `v=spf1 include:_spf.kuju.email ~all` is present | someone has done this before. Do not remove anything - go to the HUMAN ACTION below, give item 1 ONLY, and then continue at Step 5 | | anything else | the full HUMAN ACTION below | Carry any MX record, and any TXT record beginning `v=spf1`, into item 3 of that block — those are what must be replaced. Leave every other TXT record alone. Item 1 is never skipped, even when the records already look right: Kuju's MX and SPF values are the same for every customer, so seeing them proves only that somebody added them, not that this domain exists in Kuju. Without provisioning, there is no DKIM record to add and no record table to check against. > **HUMAN ACTION** - you cannot do this step. Give the person: > > 1. The instruction to add the domain in the Kuju admin, choose > **Keep your current DNS**, and provision it. Kuju then > shows a table of the exact records to add — Type, Name and Value — and does > not touch their DNS itself. If the domain is already there, they do not add > it again: ask them to confirm it is provisioned and that > "Keep your current DNS" is the option selected, then read > the record table from its DNS page. > 2. The DNS panel link or host name you carried from Step 2, and the > instruction to add every row of that table there: an MX at the domain, an > SPF TXT at the domain, a DKIM TXT at a `._domainkey` name, and a DMARC TXT > at `_dmarc.`. Two things to tell them about the values. Copy them > from Kuju's table rather than retyping — the DKIM value is unique to their > domain and the DMARC value contains it. And Kuju displays the TXT values > wrapped in quotation marks: those quotes show where the value starts and > ends and are not part of it, so if their panel does not already show quotes > around its other TXT records, paste the value without them. > 3. Any MX or `v=spf1` TXT records you identified above, quoted back to them > exactly, with the instruction to remove those — and only those — in the > same edit. > 4. An explicit statement, in your own words: the nameservers must not change. > > If the table shows no DKIM row at all, stop and report that rather than > improvising one. Otherwise, ask them to tell you when they have saved the > records. Continue at **Step 5**; on this path the person publishes the records, not Kuju. Skip Step 4 — it waits for a nameserver change, and there is none here. ## Step 4 - Wait for the delegation to be visible Only Step 3a and Step 1b arrive here. Before waiting on anything, confirm Kuju actually has a zone for the domain. This costs one lookup and can save two hours: dig MX @ns1.kuju.email `status: NOERROR` with an ANSWER SECTION means the zone exists and the delegation will work once it propagates. `status: REFUSED` means it does not — go straight to Step 4c. Anything else — a timeout, `status: SERVFAIL`, or NOERROR with no ANSWER SECTION — tells you nothing either way: do not send anyone to Step 4c on it. Carry on with the poll below, and if the delegation never appears, come back and try this check again. Then re-run Step 1 every 15 minutes: dig NS +short dig NS +short @1.1.1.1 dig NS +short @8.8.8.8 | Observation | Next | | --- | --- | | all three answers end in `.kuju.email.` | Step 5 - Kuju publishes the mail records | | some answers are old, some new, or some empty | propagation in progress - wait 15 minutes and repeat | | all three are empty, or `dig NS ` shows `status: SERVFAIL` | Step 4c | | unchanged after 2 hours, from Step 3a | Step 4a | | unchanged after 2 hours, from Step 1b | Step 4b | | unchanged after 48 hours | Step 4d | ### Step 4a - The registrar has not applied it yet (from Step 3a) > **HUMAN ACTION** - ask the person to reopen the registrar's nameserver setting > for the domain and read back the two values saved there. Then ask them to > search their email, including spam, for a "confirm this change" message from > the registrar: an unclicked confirmation is the commonest cause, and the > change will never apply without it. Then re-run this step. ### Step 4b - The parent zone's records need checking (from Step 1b) > **HUMAN ACTION** - ask the person to reopen the PARENT zone's DNS records > section and read back both NS records they added. Two things to confirm: the > name should be only the subdomain label, such as `mail`, and not the full > `mail.example.com`; and there should be two records, not one. The parent > domain's own nameserver setting is correct as it is and should not be touched. > Then re-run this step. ### Step 4c - The nameservers moved but Kuju has no zone The domain was never provisioned in Kuju, or was provisioned with the wrong choice, so Kuju's nameservers refuse to answer for it and the delegation cannot resolve. This is not a registrar problem and the registrar cannot fix it. > **HUMAN ACTION** - ask the person to open the domain in the Kuju admin and > provision it with **Use Kuju DNS**. If it is already > there, ask them to confirm which DNS option is selected; > "Keep your current DNS" is the one that produces this. Then > re-run this step. ### Step 4d - Still unchanged after 48 hours > **HUMAN ACTION** - the change is not being applied, and the person needs > support from the host you identified in Step 2: the registrar if they came > from Step 3a, or whoever hosts the parent zone's DNS if they came from > Step 1b — which is often not the registrar. ## Step 5 - Verify the mail records Two cases, and they differ in what a missing record means: - **Kuju publishes the records** — the domain is delegated to Kuju, whether by Step 3a, by Step 1b, or before this conversation started. A missing record can still be on its way. - **The person publishes them** — Step 3b. Their DNS host still needs a few minutes, and your own earlier lookup may have cached a "not there" answer, so one re-check is still right; after that, something was not saved as shown. Check each one: dig MX +short dig TXT +short dig TXT _dmarc. +short | Record | Expected | If missing | If present but does not match | | --- | --- | --- | --- | | MX | exactly `10 mail.kuju.email.` | see "If a record is missing" below | MISMATCH - report the value seen; a second MX means the old provider's record is still published and mail may still route there | | SPF (TXT at the domain) | a record equal to `v=spf1 include:_spf.kuju.email ~all` | same | MISMATCH - report every `v=spf1` value seen; two of them is an outright failure, not a cosmetic mismatch (see Step 3) | | DMARC (TXT at `_dmarc.`) | a record starting with `v=DMARC1` (Kuju's default is `v=DMARC1; p=quarantine; rua=mailto:postmaster@`) | same | MISMATCH - report the value seen | `dig +short` prints a TXT value wrapped in quotation marks. Those quotes are how DNS represents the value, not part of it, and most control panels add them for you — so a value that looks unquoted in the person's panel and quoted in `dig` is the same value, and correct. Never ask anyone to add quotation marks to make the two look alike; that changes the record and, for SPF, stops it beginning `v=spf1` at all. A value that appears DOUBLY quoted in `dig` output is the one to report — that is a literal quote saved inside the value. ### DKIM DKIM's record name contains a selector, so the name is not fixed. A newly provisioned domain uses `default`. Try that before asking anyone: dig TXT default._domainkey. +short If that returns a record containing `v=DKIM1`, the selector is `default` and DKIM is published — no need to ask. One exception: an administrator can generate a key under a different selector, and where the person publishes their own records the old `default` record is not removed, so it can still answer. If the person mentions a new DKIM key, confirm the selector below rather than trusting this probe. Otherwise: > **HUMAN ACTION** - ask the person to open the domain in the Kuju admin, find > the DNS section, and read you the DKIM selector shown there (usually > `default`). On a domain that keeps its own DNS host, that > screen also shows a "Nameservers Not Pointed at Kuju" notice > and says DNS is externally managed. Tell them before they read it out that > this is expected on their setup and is not a fault. dig TXT ._domainkey. +short | Observation | Next | | --- | --- | | a record containing `v=DKIM1` | DKIM is published - Step 6 | | empty | see "If a record is missing" below, using the name `._domainkey.` | | non-empty but does not contain `v=DKIM1` | MISMATCH - confirm the selector spelling with the person and re-run once; if it still does not match, report it as MISMATCH rather than PASS or MISSING | If the person publishes their own records, tell them one more thing before you finish. Kuju cannot publish into DNS it does not control, so if the DKIM record shown in the Kuju admin ever changes — for example after an administrator generates a new key — they must copy the new record out of the Kuju admin into their DNS themselves. Where Kuju publishes the records, this happens by itself. ### If a record is missing **Where Kuju publishes the records:** wait 15 minutes and re-check — Kuju creates them when the domain is provisioned. If it is still missing, **HUMAN ACTION** - ask the person to open the domain's DNS page in the Kuju admin and press "Re-check", then re-run. If the record is still absent, that button only re-reads what is published — ask them to find the **Auto-Configure Mail Records** section on the same page and press its **Auto-Configure** button, which republishes the records themselves. Tell them BEFORE they press it that a dialog will ask "This will replace existing MX, SPF, DKIM, and DMARC records. Continue?" and that they must accept it — replacing those four records is what the button is for. If the person hand-added any mail records to this domain, stop and check with them first. **Where the person publishes them (Step 3b):** rule out a cached answer first. A lookup made before the record existed can be remembered by the resolver for an hour or more, so ask the domain's own nameservers directly. Use the NS hostnames from Step 1 — ask EACH of them, and do not add `+short`, because you need the header. This check needs `dig`, and it is the one place in this runbook where `nslookup` is not a substitute: it takes the server as a trailing argument rather than `@server`, and prints no `status:` line, so the table below cannot be read through it. With only `nslookup`, do not conclude anything is missing — report the record UNVERIFIED and give the person the `dig` command to run. dig MX @ dig TXT @ dig TXT _dmarc. @ dig TXT ._domainkey. @ Read the `status:` line and the ANSWER SECTION, exactly as in Step 1: | Observation | Meaning | | --- | --- | | `status: NOERROR` and the record is in the ANSWER SECTION | the record IS published and only your resolver's cache is stale - report it as PASS, and tell the person it will be visible everywhere shortly | | `status: NOERROR` with no ANSWER SECTION, or `status: NXDOMAIN`, on every nameserver you asked | the record is genuinely not there - continue below. (A missing record at the domain itself, MX or SPF, answers NOERROR; a missing `_dmarc` or `._domainkey` name answers NXDOMAIN. Both mean absent) | | the nameservers disagree with each other | the change is still publishing across their provider - wait 15 minutes and ask again | | `status: REFUSED`, `status: SERVFAIL`, or `connection timed out` | this tells you NOTHING about the record - the server would not answer you. Do not treat it as missing; wait and re-check, or ask a different nameserver from Step 1 | > **HUMAN ACTION** - only when every nameserver answered and none had the > record: ask the person to open the record table in the Kuju admin next to > their DNS panel and compare the missing row's Name against what they saved. > The usual cause is a panel that appends the domain for you, so > `_dmarc.` ends up saved as `_dmarc..`. Then re-run the > check above. Once the record you came here for is resolved, go back to the check that sent you: if that was the MX, SPF or DMARC table, DKIM above it has not been done yet. If a record is still missing after two re-checks, stop here and use the [delivery troubleshooting runbook](https://www.kaimoku.tech/kuju-email/agent/troubleshooting-delivery.md). That does not apply to a record the nameservers confirmed above: one that is published but not yet visible to your resolver is correct, and reporting it as a problem would send the person chasing something that is already right. ## Step 6 - Report Tell the person, in this order: 1. Which path they took: Step 3a, Step 3b, Step 1b, or "already delegated before we started" if Step 1 sent you straight to Step 5. 2. Which registrar or DNS host you identified. 3. What they changed. 4. The `dig NS` answers you have: the three from Step 4, or — where Step 4 was not run — the ones from Step 1. 5. The four verification results from Step 5, each one PASS, MISSING or MISMATCH, with the value observed, and the DKIM selector you used. Where a line does not apply on the path taken, say so explicitly rather than leaving it out: Step 2 and Step 4 are not run on the already-delegated path, and Step 4 is not run on Step 3b. Do not summarise a MISSING or a MISMATCH as "done" — MISMATCH means a record exists but is wrong, which is not the same as working. Then, if anything changed, report what you saw beforehand. You cannot observe it again once it is gone: - **From Step 3a** - the nameservers from Step 1, and the MX and TXT answers you captured. Undoing the delegation is normally just setting those nameservers back: the previous DNS host usually still holds the zone, so its records return with it. What you captured is what rebuilds it if that zone has since been deleted, and it is also the checklist of A, AAAA, CNAME and non-SPF TXT records that still have to be recreated on Kuju's side. - **From Step 3b** - any MX or `v=spf1` records you had the person remove, quoted exactly. Those are gone from their DNS now, and your report is the only copy. - **From Step 1b** - the two NS records added to the parent zone, and the parent zone's own nameservers from Step 1. Undoing this means removing those two records; nothing else in the parent zone was touched. --- # Move an existing mailbox into Kuju **Before you start.** This runbook assumes: - the customer has an active Kuju mailbox (demo domain or their own domain) - the customer knows the OLD provider and can sign in to it - you can run dig and openssl If one of these is not true, say so before you continue — a step below may already tell you what to do about it, so keep reading before you stop. Only abort here if nothing further down handles it. You are an agent helping a person move mail from an existing mailbox into Kuju Email. Replace `` with the domain of the mailbox being moved everywhere below (for example `gmail.com` or the customer's own domain). Never invent a value you did not observe. Every command here is read-only. **You never handle credentials.** The import runs inside Kuju; the person types the old provider's password into Kuju's import form themselves. If they offer you the password, decline and tell them where to enter it. Steps marked **HUMAN ACTION** are things you cannot do — for example signing in to the old provider or to Kuju's import page, entering a password, reading a value from the Kuju admin screen, or checking their own mailbox. Give the person exact instructions, then wait for them to confirm. ## Step 1 - Identify the source provider > **HUMAN ACTION** - ask: "Where is the old mailbox - Gmail, Outlook/Microsoft > 365, Fastmail, or something else?" | Answer | IMAP server to use in the import form | What the person must prepare first | | --- | --- | --- | | Gmail / Google Workspace | `imap.gmail.com`, port 993 | an App Password (Google account > Security > 2-Step Verification > App passwords); their normal password will not work | | Outlook.com / Microsoft 365 | `outlook.office365.com`, port 993 | their normal password, or an app password if the account uses two-step sign-in | | Fastmail | `imap.fastmail.com`, port 993 | an App Password (Settings > Privacy & Security > Integrations) | | iCloud | `imap.mail.me.com`, port 993 | an app-specific password (appleid.apple.com) | | something else | Step 1a | their normal mailbox password | ### Step 1a - Discover the IMAP server for another provider dig SRV _imaps._tcp. +short dig MX +short | Observation | Next | | --- | --- | | the SRV answer names a host (the last field, e.g. `imap.example.net.`) | use that host, port 993 | | SRV empty, MX names a host | the IMAP server is usually `imap.` or `mail.` at the same provider as the MX host; try both in Step 2 | | both empty | **HUMAN ACTION** - the person must look up "IMAP settings" in the old provider's help pages | ## Step 2 - Confirm the server is reachable (no login) openssl s_client -connect :993 -brief **HUMAN ACTION** - ask the person to open the import page in Kuju, enter the > server from Step 1 and their credentials, and run the size estimate. Ask them > to read you the total and, if shown, the date of the oldest message. Two things to tell the person about the number: - **Gmail double-counts.** Labels are virtual folders, so a message with three labels appears in three folders. An estimate that sums every folder overstates a heavily-labelled account 2-3x. The accurate figure is `[Gmail]/All Mail` alone; if the estimate lists folders, use that folder's size. - **It is wire size, not disk size.** The figure is what the old server will send, not what it occupies on disk; the two differ by index overhead and compression. Treat it as an estimate, not an invoice. Trash and Spam usually hold real bytes nobody wants. Ask whether to exclude them; the default is to leave them out. ## Step 4 - Explain the test cap in time, not bytes During the closed beta a test migration imports at most **2 GB per account**. Kuju imports newest-first, so the imported slice is the person's RECENT mail - the mail they actually use. Describe the cap using the estimate from Step 3, like this: Your mailbox is 18 GB. The test brings your most recent 2 GB - roughly your last 5 months. Nothing older is lost; it stays where it is and comes across when the account converts. Work the months out from the estimate: if the mailbox spans M months and holds E GB, the cap covers about `M x 2 / E` months of the newest mail. **Reaching the cap is a PAUSE. It is not an error and it is not a restart.** The import records where it stopped (the folder and the last message id), remembers how many bytes it brought across, and skips anything already imported if it is run again. When the person converts to a paying account, the SAME job resumes from that point; nothing is re-imported and nothing is duplicated. If the person asks "did it fail?", the answer is no. ## Step 5 - Start the import > **HUMAN ACTION** - the person starts the import from the same page. Ask them > to tell you the status shown, and to check it again after a few minutes. | Status shown | Meaning | Next | | --- | --- | --- | | running | mail is arriving, newest first | wait; check again in 10 minutes | | paused, mentioning the cap or the beta | the 2 GB test slice is complete | Step 6 - this is the expected end state for a test | | paused, any other reason | the old provider disconnected (rate limiting is common) | the person presses resume; the import continues from its checkpoint | | failed, mentioning login or authentication | wrong credentials or a missing app password | back to Step 1's third column | | completed | everything came across (the mailbox was under the cap) | Step 6 | ## Step 6 - Verify what arrived > **HUMAN ACTION** - ask the person to open their Kuju mailbox and confirm: > the newest messages are present, folders match the old layout, read/unread > flags survived, and dates are the original dates (not today's). If something is missing, running the import again is safe: duplicates are detected and skipped, so a re-run only adds what is absent. ## Step 7 - Remind them what has NOT changed Importing copies mail; it does not redirect it. New mail keeps arriving at the old provider until the domain's MX record points at Kuju (`10 mail.kuju.email.`). For a demo-domain mailbox that is expected. For their own domain, the next runbook is [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md). --- # Troubleshoot mail delivery for a Kuju domain **Before you start.** This runbook assumes: - the domain is already delegated to Kuju (see dns-delegation) or its mail records were added externally - you can run dig and openssl If one of these is not true, say so before you continue — a step below may already tell you what to do about it, so keep reading before you stop. Only abort here if nothing further down handles it. You are an agent helping a person troubleshoot mail delivery for ``, a domain that already uses Kuju Email. Replace `` with the customer's domain everywhere below (for example `example.com`). Never invent a value you did not observe. Every command here is read-only. Steps marked **HUMAN ACTION** are things you cannot do — for example clicking through DNS or registrar control panels, reading a bounce message from the person's own inbox, reading a value from the Kuju admin screen, or sending and inspecting a test message from their own mailbox. Give the person exact instructions, then wait for them to confirm. Pick the section that matches the symptom the person reports. Collect every observation you make; the last section tells you how to report them. ## A - "Mail sent TO my domain is not arriving" ### A1 - Where does the world think mail for this domain goes? dig MX +short | Observation | Meaning | Next | | --- | --- | --- | | exactly `10 mail.kuju.email.` | Kuju receives mail for the domain | A2 | | empty | no MX; senders will try the domain's A record, which is not a mail server | the domain is not provisioned - run [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) from Step 1 | | a different host | the OLD provider still receives the mail | the person's old mailbox has it; the change never landed - run A2 first to see which DNS model they use, then [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) Step 4 if delegated, or its Step 3b if they kept their own host | | Kuju's host plus other hosts | split delivery; some senders reach the old provider | **HUMAN ACTION** - the extra MX records must be removed where the DNS lives | ### A2 - Is the delegation consistent? dig NS +short dig NS +short @1.1.1.1 | Observation | Next | | --- | --- | | both answers end in `.kuju.email.` | A3 | | they differ | propagation is still in progress; wait 15 minutes and re-run A1 | | neither is Kuju | the person chose "Keep your current DNS", so the nameservers are not meant to be Kuju's; A1's result is what matters - if A1 passed, A3 | ### A3 - Does Kuju's mail server accept connections? openssl s_client -connect mail.kuju.email:25 -starttls smtp -brief **HUMAN ACTION** - if a specific message did not arrive, ask the person to > get the bounce message (the "Undelivered Mail" reply) from the SENDER. The > bounce names the server that rejected the mail and why. Read it to them: > > | Bounce text contains | Meaning | > | --- | --- | > | `550` and `does not exist` or `unknown user` | the address is not set up in Kuju; **HUMAN ACTION** - the person needs to create the mailbox or alias in the Kuju admin | > | `550` and `spam` or `blocked` or `rejected` | Kuju's filters rejected it; the sender's own domain probably fails SPF/DKIM; the sender should check their setup | > | `quarantine` or `held` | the message is in the person's Kuju quarantine folder, not lost | > | `450` or `try again later` | a temporary deferral; the sender's server will retry for days - wait | > | no bounce at all after 24 hours | the sender's server may still be retrying; ask them to check their outbound queue | ## B - "Mail I send from my domain goes to spam or bounces" ### B1 - Is the domain authenticated? dig TXT +short dig TXT _dmarc. +short | Record | Expected | If not | | --- | --- | --- | | SPF | a TXT equal to `v=spf1 include:_spf.kuju.email ~all` | missing SPF is the most common cause of spam placement; the domain is not fully provisioned - B1a, then [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) Step 5 | | DMARC | a TXT starting `v=DMARC1` | same | | more than one SPF record | INVALID - SPF fails outright for every message (a PermError per RFC 7208), so neither record is used | **HUMAN ACTION** - the extra record must be deleted where the DNS lives | DKIM's record name contains a selector, so ask for it: > **HUMAN ACTION** - ask the person to read the DKIM selector from the domain's > DNS section in the Kuju admin. A newly provisioned domain uses `default`; an > administrator can generate a key under a different name. dig TXT ._domainkey. +short | Observation | Next | | --- | --- | | a record containing `v=DKIM1` | B2 | | empty | B1a, then [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) Step 5 - on a delegated domain a re-check in the admin usually fixes it; on the "Keep your current DNS" path the person must add the record themselves | ### B1a - Before routing back: rule out a cached answer Rule out a cached answer before routing back to `dns-delegation`: a lookup made before the record existed can be remembered by the resolver for an hour or more, so ask the domain's own nameservers directly. dig NS +short Ask EACH host it prints, and do not add `+short` below, because you need the header: dig MX @ dig TXT @ dig TXT _dmarc. @ dig TXT ._domainkey. @ This check needs `dig`, and it is the one place in this runbook where `nslookup` is not a substitute: it takes the server as a trailing argument rather than `@server`, and prints no `status:` line, so the table below cannot be read through it. With only `nslookup`, do not conclude anything is missing - report the record UNVERIFIED and give the person the `dig` command to run. Read the `status:` line and the ANSWER SECTION: | Observation | Meaning | | --- | --- | | `status: NOERROR` and the record is in the ANSWER SECTION | the record IS published and only your resolver's cache is stale - report it as PASS, and tell the person it will be visible everywhere shortly | | `status: NOERROR` with no ANSWER SECTION, or `status: NXDOMAIN`, on every nameserver you asked | the record is genuinely not there - continue below. (A missing record at the domain itself, MX or SPF, answers NOERROR; a missing `_dmarc` or `._domainkey` name answers NXDOMAIN. Both mean absent) | | the nameservers disagree with each other | the change is still publishing across their provider - wait 15 minutes and ask again | | `status: REFUSED`, `status: SERVFAIL`, or `connection timed out` | this tells you NOTHING about the record - the server would not answer you. Do not treat it as missing; wait and re-check, or ask a different nameserver | If the record really is absent on every nameserver, go to [dns-delegation](https://www.kaimoku.tech/kuju-email/agent/dns-delegation.md) Step 5 to publish or re-check it. If a record is still missing after two re-checks, stop here: collect the evidence for support (see Report) rather than returning to the DNS runbook a third time. ### B2 - What does a receiver actually see? > **HUMAN ACTION** - ask the person to send a test message from their Kuju > mailbox to a Gmail or Outlook address they control, open it there, and view > the original message / headers. Ask them to read you the line that starts > `Authentication-Results:`. | The line contains | Meaning | | --- | --- | | `spf=pass`, `dkim=pass`, `dmarc=pass` | authentication is correct; spam placement is reputation or content, not setup - B3 | | `spf=fail` or `spf=softfail` | the mail did not leave through Kuju's servers, or SPF is missing - re-run B1 | | `dkim=fail` | the published key does not match the one Kuju signs with - **HUMAN ACTION**: have the person compare the DKIM record in the Kuju admin's DNS section with what is published (a newly generated key, or a value pasted incomplete), republish it, and re-test; still failing - report | | `dmarc=fail` | follows from the two above | ### B3 - Reputation and content Nothing here is a DNS problem. Tell the person: a new domain has no sending history and lands in spam more often for the first weeks; sending to people who have never written to them, or sending many identical messages, makes it worse. Ask recipients to mark the message "not spam" once. If the volume is legitimate and the problem persists, report it (below). ## C - "I hit a sending limit" Kuju applies per-account sending limits. This runbook deliberately does not state the numbers - they are configured per deployment and can change. The error message the person sees names the limit that was hit. > **HUMAN ACTION** - ask the person to read you the exact error text. | Error text contains | Meaning | Next | | --- | --- | --- | | `daily` or `per day` | the daily cap; it resets at midnight UTC | wait, or contact support if the volume is legitimate | | `rate` or `too many` or `slow down` | short-term rate limit | wait a few minutes and retry | | `quota` or `storage` | the MAILBOX is full, not a sending limit | the person must delete mail or raise the quota with support | ## Report When you cannot resolve it, give the person this block to send to Kuju support, filled in with what you observed (paste the raw command output, not a summary): Domain: Symptom: dig MX +short -> dig NS +short -> dig TXT +short -> dig TXT _dmarc. +short -> Authentication-Results line -> Bounce or error text -> --- # Kuju Email glossary Plain definitions for the security terms used across Kuju Email. HTML version: https://www.kaimoku.tech/kuju-email/glossary ## DKIM — DomainKeys Identified Mail A cryptographic signature attached to each outgoing message, verified by the recipient's mail server against a public key in your DNS. *In your message headers:* ``` Authentication-Results: mx.kuju.email; dkim=pass header.d=example.com header.s=mail ``` *DNS record (for domain admins):* ``` mail._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0G..." ``` **Why it matters:** It proves the message wasn't altered between the sender and you. ## DMARC — Domain-based Message Authentication, Reporting, and Conformance A DNS policy telling receiving servers what to do when a message claiming to be from your domain fails SPF or DKIM checks — reject it, quarantine it, or just report it. *In your message headers:* ``` Authentication-Results: mx.kuju.email; dmarc=pass action=none header.from=example.com ``` *DNS record (for domain admins):* ``` _dmarc.example.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" ``` **Why it matters:** SPF and DKIM detect impersonation; DMARC decides what to do about it. ## Encrypted at rest Data written to disk is encrypted, so a lost or discarded disk can't be read without the key. Kuju's stored mail and its servers' boot disks use our hosting provider's volume encryption, with keys managed by the provider rather than by Kaimoku — it protects against a lost disk, not against someone with access to the running system. **Why it matters:** A server's physical security alone isn't enough — at-rest encryption is the second line of defense for stored mail. ## End-to-end encryption A class of encryption in which only the sender and recipient hold the keys — no server in between (not even your mail provider) can read the message. Kuju Email does not implement end-to-end encryption today; we use TLS in transit and at-rest encryption instead. **Why it matters:** End-to-end encryption is a stronger guarantee for specific use cases, but it breaks server-side spam filtering, search, and shared-mailbox workflows. We've prioritized strong transport and storage encryption with full feature support. ## Link-safety check When you ask, the links in a message are checked against Google's continuously-updated lists of known phishing, malware, and unwanted-software sites. The lists are held on our servers, so a link's address is not sent to Google. The check is optional and runs only on request. **Why it matters:** Even a message that passes every other check can carry a link to a malicious site. This is the last check before you click. ## MX records — Mail Exchange records DNS entries that tell sending mail servers which servers handle incoming mail for your domain. When you point MX records at Kuju, mail addressed to your domain comes to us. *DNS record (for domain admins):* ``` kuju.email. IN MX 10 mx1.kuju.email. kuju.email. IN MX 20 mx2.kuju.email. ``` **Why it matters:** This is the switch that moves your email to Kuju. Until your MX records change, your mail still flows to your old provider. ## Passkeys — WebAuthn A cryptographic key pair stored on your device (or in a password manager) that replaces the password entirely. Signing in is a tap of your fingerprint or a face unlock. **Why it matters:** There is no shared secret to steal in a phishing attack. Passkeys cannot be intercepted, replayed, or guessed. ## Relay-hop trace Mail messages pass through a chain of servers ("hops") on the way to you, and each hop adds a Received header. Inspecting the chain reveals where a message really came from. *In your message headers (read bottom-up):* ``` Received: from mx-out.example.com (198.51.100.42) by mx.kuju.email; Mon, 12 May 2026 09:14:22 +0000 Received: from internal.example.com by mx-out.example.com; Mon, 12 May 2026 09:14:20 +0000 ``` **Why it matters:** Some phishing attempts forge the visible "From" address but can't hide their true path. Tracing the hops surfaces those fakes. ## Reputation checks External services maintain lists scoring the trustworthiness of sending mail servers, IP addresses, and domains based on historical behavior. Incoming mail is scored against those lists. **Why it matters:** Known spammers and compromised servers get caught even when their individual messages look innocuous. ## Spam & phishing patterns A library of structural signals — suspicious links, unusual formatting, common scam phrasing — that mail filters use to classify messages alongside the reputation score. **Why it matters:** New scams get caught quickly when they reuse patterns from older scams, even when they come from new addresses. ## SPF — Sender Policy Framework A DNS record listing which mail servers are allowed to send mail for your domain. Receivers check this list when a message arrives. *In your message headers:* ``` Authentication-Results: mx.kuju.email; spf=pass smtp.mailfrom=sender@example.com ``` *DNS record (for domain admins):* ``` example.com. IN TXT "v=spf1 include:_spf.kuju.email ~all" ``` **Why it matters:** Without SPF, anyone can claim to send mail from your address. With it, fakes get caught at the door. ## TLS in flight Encryption applied to the connection between mail servers and between your mail client and the server, so messages can't be read mid-transit. **Why it matters:** No one on the network in between can read what's passing through, even when the message contents aren't end-to-end encrypted. ## TOTP — Time-based One-Time Password A six-digit code that changes every 30 seconds, generated by an authenticator app on your phone, entered alongside your password when signing in. *In your authenticator app:* ``` 123 456 (expires in 22 seconds) ``` **Why it matters:** A stolen password alone isn't enough to sign in as you. The attacker would also need physical access to your phone. ## Virus stripping Attachments are scanned with anti-virus engines before the message is delivered. Confirmed malicious files are removed automatically; the message body still arrives so you don't miss what was sent. **Why it matters:** Most malicious attachments arrive as ordinary-looking documents. Stripping them keeps the message useful without putting your machine at risk. --- # Kuju Email FAQ Answers to the questions people ask before choosing Kuju Email. HTML version: https://www.kaimoku.tech/kuju-email/faq ## Can I sign up for Kuju Email today? Not publicly yet. Kuju Email is invite-only while it is in private development. If you have received a trial invite, start at mail.kuju.email/signup with the code from your invite email. Otherwise, leave your email address to be told when it opens. See also: [Get notified at launch](https://www.kaimoku.tech/kuju-email#notify) ## How much will it cost? There are four plans. Individual / Family is $10 a month for up to 5 mailboxes, then $5 per mailbox up to 10. Small Business is $5 per mailbox, for 5 to 25 mailboxes. Professional is a $75 monthly platform fee plus $5 per mailbox, from 10 mailboxes. Enterprise is a $250 monthly platform fee plus $5 per mailbox, from 25 mailboxes. Every plan is 17% cheaper (two months free) when billed annually. See also: [Pricing and calculator](https://www.kaimoku.tech/kuju-email/pricing) ## Will there be a free trial? Yes. When Kuju Email opens, every new signup will get a 14-day trial with Professional-level access and an address on demo.kuju.email. When a trial ends, the account freezes: you can still sign in and read your mail, but sending and receiving pause. Your data is kept for 30 days so you can choose a plan. ## Can I use my own domain? Yes, on every paid plan, with unlimited domains at no extra charge. You can either point your domain's nameservers at Kuju and let Kuju publish the mail records, or keep your current DNS host and add the MX, SPF, DKIM and DMARC records yourself. Kuju shows you the exact values and checks each record for you. See also: [What MX records do](https://www.kaimoku.tech/kuju-email/glossary#mx-records) ## Can I bring my existing mail with me? Yes. Kuju imports mail from any provider that offers IMAP, including Gmail, Outlook and Microsoft 365, Fastmail and iCloud. You enter the old mailbox's details into Kuju's import page yourself; for Gmail, Fastmail and iCloud that means an app password. Kuju can measure the old mailbox before anything moves, then imports newest mail first with folders, read/unread flags and original dates intact. Running an import again is safe: messages already imported are skipped. During the closed beta a test migration brings across at most 2 GB per account. Reaching that limit pauses the import; it resumes from the same point when the account converts to a paid plan. Importing copies your mail but does not redirect it. New mail keeps arriving at your old provider until your domain's MX record points at Kuju. ## Can I use my usual mail and calendar apps? Yes, if the app connects to our servers directly from your device. Apple Mail on iPhone, iPad and Mac sets itself up from a profile you download in webmail; Thunderbird and FairEmail find the settings themselves; Samsung Email takes the settings by hand. Apps that copy your mail or your password to their own company's servers are not supported. That includes Outlook for mobile and Mac, new Outlook for Windows, and Spark. Calendars and contacts sync with standard apps over CalDAV and CardDAV. See also: [Mail apps we support, and why](https://www.kaimoku.tech/kuju-email/mail-apps) · [Setting up a mail app](https://www.kaimoku.tech/kuju-email/guide#clients) ## Is my email encrypted? Who can read it? Mail apps connect over TLS 1.3 and webmail is HTTPS only. Message bodies and attachments are stored on an encrypted volume, with the encryption keys managed by our hosting provider. Kuju Email is not end-to-end encrypted, so the service can read stored mail in order to filter, search and serve it, and a small number of Kaimoku operators have technical access to it. Two-factor sign-in protects webmail; mail apps sign in with your account password or an app password, and once two-factor is on only an app password works in them. See also: [Security overview](https://www.kaimoku.tech/kuju-email/security) ## Does Kuju use AI on my email? Not on customer domains today. Kuju Email has AI features — an AI spam and phishing check, message analysis you ask for, and tools such as summaries, reply drafts, task extraction and plain-language search — but we do not offer them on customer domains yet, and a domain administrator cannot turn them on. Nothing from your domain is sent to an AI provider. Kuju's own spam and phishing checks run on every message on our servers without one. When we offer them, each will be off for your domain until your domain administrator turns it on and confirms what is sent. Our privacy policy lists the AI providers we use. Separately, and not AI: when you ask Kuju to check the links in a message, they are compared on our servers against a list of known-bad sites from Google. Only if a link matches is a short hash prefix of it sent to Google, never the address. See also: [When mail content leaves our systems](https://www.kaimoku.tech/kuju-email/security#third-parties) ## Where is my data stored? Your mailbox and account live in the United States: production runs on Akamai (Linode) in the Washington, DC region. Off-site backups are kept in Cloudflare R2; Cloudflare chooses where they are stored, and we have not pinned them to a region. See also: [Security overview](https://www.kaimoku.tech/kuju-email/security) ## Can I take my data with me if I leave? Yes. You can export your mail, calendars and contacts at any time using standard protocols (IMAP, CalDAV and CardDAV), or through the API where your plan includes it. After your account ends you have 30 days to export. Thirty days after an account ends, its records, including folders and message records, are deleted from our database. That automatic deletion does not yet remove the stored content of messages (bodies and attachments); ask us to delete your account and we delete that content too. Database backups are kept for 30 days. Stored mail is also copied daily to an off-site backup that only adds files, so copies of deleted messages can remain in it. See also: [Privacy Policy: data retention](https://www.kaimoku.tech/legal/privacy) ## Can I cancel at any time? Yes, from your account settings or by contacting us. Cancellation takes effect at the end of your current billing period. ## What happens if I run out of storage? Each mailbox includes 5 GB on Individual / Family and 10 GB on the other plans; extra storage is $1 per GB per month. If you go over, you get a warning and a 14-day grace period to clean up or upgrade. If you do nothing, large attachments are throttled, but your email keeps working. See also: [Pricing](https://www.kaimoku.tech/kuju-email/pricing) ## Is there an API? Yes. The API is available on Small Business and above, and is documented endpoint by endpoint. See also: [API documentation](https://www.kaimoku.tech/kuju-email/docs) ## How do I get help? Start with the user guide and this FAQ. If your question is not answered there, write to us through the contact page; a person reads every message and aims to reply by email within two business days. See also: [Contact us](https://www.kaimoku.tech/contact) --- # Kuju Email API reference Generated from the OpenAPI spec. Base URL, authentication and examples are on the HTML page: https://www.kaimoku.tech/kuju-email/docs ## Overview ### Base URL See https://www.kaimoku.tech/kuju-email/docs#base-url ### Authentication See https://www.kaimoku.tech/kuju-email/docs#authentication ## Public API ### Auth | Method | Path | Auth | Description | | --- | --- | --- | --- | | POST | `/api/auth/login` | public | Login and obtain JWT (or TOTP challenge) | | POST | `/api/auth/logout` | JWT | Logout (no-op) | | POST | `/api/auth/refresh` | JWT | Refresh JWT | | POST | `/api/invite` | public | accept invite and set password (public, token-based) | | POST | `/api/forgot-password` | public | request password reset email | | POST | `/api/reset-password` | public | reset password with token | ### Two-Factor (TOTP) | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/auth/totp/status` | JWT | check if TOTP is enabled | | POST | `/api/auth/totp/setup` | JWT | generate secret + QR code | | POST | `/api/auth/totp/confirm?session_id=...` | JWT | verify code, enable TOTP, return backup codes | | POST | `/api/auth/totp/verify` | public | verify TOTP code during login, issue JWT | | DELETE | `/api/auth/totp` | JWT | disable TOTP (requires current code) | Parameters for `POST /api/auth/totp/confirm?session_id=...`: - `session_id` (query, required) ### Folders | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/folders` | JWT | List folders | | POST | `/api/folders` | JWT | Create folder | | PUT | `/api/folders/:id` | JWT | Rename folder | | DELETE | `/api/folders/:id` | JWT | Delete folder (must be empty) | | GET | `/api/folders/:id/messages` | JWT | List messages in folder | Parameters for `PUT /api/folders/:id`: - `id` (path, required) Parameters for `DELETE /api/folders/:id`: - `id` (path, required) Parameters for `GET /api/folders/:id/messages`: - `id` (path, required) - `page` (query) - `per_page` (query) ### Messages | Method | Path | Auth | Description | | --- | --- | --- | --- | | POST | `/api/messages` | JWT | compose / send | | GET | `/api/messages/:id` | JWT | Get message metadata | | GET | `/api/messages/:id/raw` | JWT | raw RFC 822 source | | GET | `/api/messages/:id/body` | JWT | rendered body | | GET | `/api/messages/:id/attachments/:aid` | JWT | download attachment | | PATCH | `/api/messages/:id/flags` | JWT | update flags (read, starred, etc.) | | PATCH | `/api/messages/:id/move` | JWT | Move message to folder | | PATCH | `/api/messages/:id/copy` | JWT | Copy message to folder | | DELETE | `/api/messages/:id` | JWT | Delete message | | POST | `/api/messages/search` | JWT | advanced search | | GET | `/api/messages/search?q=...` | JWT | quick search | Parameters for `GET /api/messages/:id`: - `id` (path, required) Parameters for `GET /api/messages/:id/raw`: - `id` (path, required) Parameters for `GET /api/messages/:id/body`: - `id` (path, required) Parameters for `GET /api/messages/:id/attachments/:aid`: - `id` (path, required) - `aid` (path, required) Parameters for `PATCH /api/messages/:id/flags`: - `id` (path, required) Parameters for `PATCH /api/messages/:id/move`: - `id` (path, required) Parameters for `PATCH /api/messages/:id/copy`: - `id` (path, required) Parameters for `DELETE /api/messages/:id`: - `id` (path, required) Parameters for `GET /api/messages/search?q=...`: - `q` (query, required) - `folder_id` (query) - `page` (query) - `per_page` (query) ### Message Analysis | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/messages/:id/analysis` | public | cached results + feature availability | | GET | `/api/messages/:id/analysis/instant` | public | header analysis (auth, sender, hops, URLs) | | POST | `/api/messages/:id/analysis/ai` | public | AI threat assessment (?force=true to re-analyze) | | POST | `/api/messages/:id/analysis/av-rescan` | public | ClamAV attachment scan | | POST | `/api/messages/:id/analysis/url-safety` | public | on-demand link-safety check | ### Attachment Knowledge | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/messages/:id/attachments/:aid/knowledge` | public | cached extraction result | | POST | `/api/messages/:id/attachments/:aid/knowledge` | public | extract text + AI summarize | ### Drafts | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/drafts` | JWT | List drafts | | POST | `/api/drafts` | JWT | Create draft | | GET | `/api/drafts/:id` | JWT | Get draft | | PUT | `/api/drafts/:id` | JWT | Update draft | | DELETE | `/api/drafts/:id` | JWT | Delete draft | | POST | `/api/drafts/:id/send` | JWT | send a saved draft | Parameters for `GET /api/drafts`: - `page` (query) - `per_page` (query) Parameters for `GET /api/drafts/:id`: - `id` (path, required) Parameters for `PUT /api/drafts/:id`: - `id` (path, required) Parameters for `DELETE /api/drafts/:id`: - `id` (path, required) Parameters for `POST /api/drafts/:id/send`: - `id` (path, required) ### Calendars & Events | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/calendars` | public | list calendars | | POST | `/api/calendars` | public | create calendar | | PUT | `/api/calendars/:id` | public | update calendar | | DELETE | `/api/calendars/:id` | public | delete calendar | | GET | `/api/calendars/:calId/events?start=...&end=...` | public | list events (optional RFC 3339 range filter) | | GET | `/api/calendars/:calId/events/:id` | public | get event | | POST | `/api/calendars/:calId/events` | public | create event | | PUT | `/api/calendars/:calId/events/:id` | public | update event | | DELETE | `/api/calendars/:calId/events/:id` | public | delete event | ### Address Books & Contacts | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/address-books` | public | list address books | | POST | `/api/address-books` | public | create address book | | PUT | `/api/address-books/:id` | public | update address book | | DELETE | `/api/address-books/:id` | public | delete address book | | GET | `/api/address-books/:abId/contacts?q=...` | public | list contacts (optional search) | | GET | `/api/address-books/:abId/contacts/:id` | public | get contact | | POST | `/api/address-books/:abId/contacts` | public | create contact | | PUT | `/api/address-books/:abId/contacts/:id` | public | update contact | | DELETE | `/api/address-books/:abId/contacts/:id` | public | delete contact | ### CalDAV / CardDAV | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/.well-known/caldav` | public | redirects to /caldav/ | | GET | `/.well-known/carddav` | public | redirects to /carddav/ | | GET | `/caldav/{accountId}/{calendarName}/` | public | CalDAV endpoint | | GET | `/carddav/{accountId}/{addressBookName}/` | public | CardDAV endpoint | ### Branding | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/branding` | JWT | branding config for the authenticated user's domain | | GET | `/api/branding/assets/:filename` | JWT | branding asset file (logo, favicon, CSS) | Parameters for `GET /api/branding/assets/:filename`: - `filename` (path, required): Whitelisted branding filename ## Account API ### Preferences | Method | Path | Auth | Description | | --- | --- | --- | --- | | PUT | `/api/account/preferences` | public | update account preferences (spam_folder, plus_tag_filing, etc.) | ### AI & Intelligence | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/messages/:id/thread-ai-state` | public | get AI triage state for a thread | | POST | `/api/messages/:id/ai-reply` | public | generate AI reply draft | | POST | `/api/messages/:id/rewrite-draft` | public | rewrite compose draft with tone control | | POST | `/api/messages/:id/thread-document` | public | generate document-centric thread analysis | | POST | `/api/analysis/natural-search` | public | interpret natural language search query | | POST | `/api/analysis/backfill-body-search` | public | backfill body search index | ### Thread Graph | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/messages/:id/thread-graph` | public | get conversation graph (nodes + edges) | ### Vacation | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/vacation` | public | get vacation responder settings | | PUT | `/api/vacation` | public | update vacation responder (enabled, subject, body, start/end dates) | ### Waiting On Reply | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/waiting` | public | list sent messages awaiting reply | | PUT | `/api/waiting/:id` | public | update waiting status (resolved, snoozed) | | POST | `/api/waiting/:id/nudge` | public | send follow-up nudge | ### Tasks | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/tasks` | public | list tasks | | POST | `/api/tasks` | public | create task | | PUT | `/api/tasks/:id` | public | update task | | DELETE | `/api/tasks/:id` | public | delete task | | POST | `/api/tasks/:id/to-calendar` | public | convert task to calendar event | ### Activity Feed | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/feed?limit=...` | public | merged chronological feed (messages, events, tasks) | ### Contact Intelligence | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/contacts/intelligence/top` | public | top contacts by email frequency | | GET | `/api/contacts/intelligence/dormant` | public | contacts with no recent communication | | GET | `/api/contacts/intelligence/:email` | public | per-email communication stats | | POST | `/api/contacts/quick-add` | public | quick-add sender to address book | ### Inbox Summary | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/inbox-summary` | public | dashboard stats (volume, security, productivity, classifications) | ### Workspaces | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/workspaces` | public | list workspaces with message counts | | POST | `/api/workspaces` | public | create workspace | | PUT | `/api/workspaces/:id` | public | update workspace (name, color, rules, etc.) | | DELETE | `/api/workspaces/:id` | public | delete workspace (messages preserved) | | GET | `/api/workspaces/:id/messages?page=1&per_page=50` | public | paginated messages | | POST | `/api/workspaces/:id/messages` | public | assign message | | DELETE | `/api/workspaces/:id/messages/:messageId` | public | remove message from workspace | | GET | `/api/messages/:id/workspaces` | public | get workspaces a message belongs to | ### NL Commands | Method | Path | Auth | Description | | --- | --- | --- | --- | | POST | `/api/commands/interpret` | public | AI interprets natural language command | | POST | `/api/commands/execute` | public | execute a command plan (move, archive, delete, flag) | ## Domain Admin API ### Accounts & Stats | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/domain-admin/stats` | public | | | GET | `/api/domain-admin/accounts` | public | | | POST | `/api/domain-admin/accounts` | public | create account (see Account Creation below) | | PUT | `/api/domain-admin/accounts/:id` | public | | | DELETE | `/api/domain-admin/accounts/:id` | public | | | POST | `/api/domain-admin/accounts/:id/resend-invite` | public | resend invite email (pending accounts only) | | POST | `/api/domain-admin/accounts/:id/recover` | public | send password reset to recovery email | | GET | `/api/domain-admin/delivery-log` | public | | | GET | `/api/domain-admin/dns/check-records` | public | verify DNS records for the admin's domain | ### Retention Policies | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/domain-admin/retention` | public | get effective retention (domain + account policies) | | PUT | `/api/domain-admin/retention/domain` | public | upsert retention for own domain | | GET | `/api/domain-admin/accounts/:id/retention` | public | get account retention override | | PUT | `/api/domain-admin/accounts/:id/retention` | public | upsert account retention override | | DELETE | `/api/domain-admin/accounts/:id/retention` | public | remove account retention override | ### Account Creation See https://www.kaimoku.tech/kuju-email/docs#account-creation ## Site Admin API ### Domains | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/domains` | JWT | List domains | | POST | `/api/admin/domains` | JWT | Create domain | | PUT | `/api/admin/domains/:id` | JWT | Update domain | | DELETE | `/api/admin/domains/:id` | JWT | Deactivate domain | Parameters for `PUT /api/admin/domains/:id`: - `id` (path, required) Parameters for `DELETE /api/admin/domains/:id`: - `id` (path, required) ### Accounts | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/accounts` | JWT | List accounts | | POST | `/api/admin/accounts` | JWT | Create account | | PUT | `/api/admin/accounts/:id` | JWT | update account; set is_admin to promote/demote | | DELETE | `/api/admin/accounts/:id` | JWT | Deactivate account | Parameters for `GET /api/admin/accounts`: - `limit` (query) - `offset` (query) Parameters for `PUT /api/admin/accounts/:id`: - `id` (path, required) Parameters for `DELETE /api/admin/accounts/:id`: - `id` (path, required) ### Branding (Admin) | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/branding/:domain` | JWT | branding config | | GET | `/api/admin/branding/:domain/files` | JWT | list branding files | | POST | `/api/admin/branding/:domain/archive` | JWT | upload tar.gz/zip archive | | GET | `/api/admin/branding/:domain/:filename` | JWT | download/preview asset | | POST | `/api/admin/branding/:domain/:filename` | JWT | upload single asset | | DELETE | `/api/admin/branding/:domain/:filename` | JWT | delete asset | Parameters for `GET /api/admin/branding/:domain`: - `domain` (path, required): Domain name (e.g. example.com) Parameters for `GET /api/admin/branding/:domain/files`: - `domain` (path, required): Domain name (e.g. example.com) Parameters for `POST /api/admin/branding/:domain/archive`: - `domain` (path, required): Domain name (e.g. example.com) Parameters for `GET /api/admin/branding/:domain/:filename`: - `domain` (path, required): Domain name (e.g. example.com) - `filename` (path, required): Whitelisted branding filename Parameters for `POST /api/admin/branding/:domain/:filename`: - `domain` (path, required): Domain name (e.g. example.com) - `filename` (path, required): Whitelisted branding filename Parameters for `DELETE /api/admin/branding/:domain/:filename`: - `domain` (path, required): Domain name (e.g. example.com) - `filename` (path, required): Whitelisted branding filename ### System & Licensing | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/stats` | JWT | System stats | | GET | `/api/admin/delivery-log` | JWT | Delivery log entries | | GET | `/api/admin/license` | JWT | License status and usage | | PUT | `/api/admin/license` | JWT | Install/update license key | | GET | `/api/admin/system-info` | JWT | System info including module versions | | GET | `/api/admin/settings` | JWT | system settings (key-value) | | PUT | `/api/admin/settings/corporate-domain` | JWT | update corporate domain | | PUT | `/api/admin/settings/safe-browsing-key` | public | save link-safety check API key | | GET | `/api/admin/dns/check-records?domain=...` | JWT | verify DNS records (MX, SPF, DKIM, DMARC) | | GET | `/api/admin/openapi.yaml` | JWT | OpenAPI spec for the REST API | Parameters for `GET /api/admin/delivery-log`: - `limit` (query) - `offset` (query) - `account_id` (query) - `status` (query) Parameters for `GET /api/admin/dns/check-records?domain=...`: - `domain` (query, required): Domain to check - `selector` (query): DKIM selector ### Delivery Thresholds | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/domains/:domainId/thresholds` | public | get spam score thresholds | | PUT | `/api/admin/domains/:domainId/thresholds` | public | update junk/drop thresholds and quarantine expiry | | GET | `/api/admin/delivery/drops` | public | hard drop statistics | ### Retention Policies | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/domains/:domainId/retention` | public | get domain retention policy | | PUT | `/api/admin/domains/:domainId/retention` | public | upsert domain retention policy | | DELETE | `/api/admin/domains/:domainId/retention` | public | remove domain retention policy | ### KumoMTA Proxy | Method | Path | Auth | Description | | --- | --- | --- | --- | | GET | `/api/admin/kumomta/metrics` | public | raw KumoMTA metrics (includes uptime, version) | | GET | `/api/admin/kumomta/{endpoint}` | public | proxy GET (bounces, suspensions, ready-q-suspensions) | | POST | `/api/admin/kumomta/{endpoint}` | public | create bounce/suspend rules | | DELETE | `/api/admin/kumomta/{endpoint}` | public | delete bounce/suspend rules | ### Plugins | Method | Path | Auth | Description | | --- | --- | --- | --- | | POST | `/api/admin/plugins/{id}/enable-all` | public | enable a plugin for all domains | ## Concepts ### Account Status See https://www.kaimoku.tech/kuju-email/docs#account-status ### Domain Fields See https://www.kaimoku.tech/kuju-email/docs#domain-fields ### Plus Addressing See https://www.kaimoku.tech/kuju-email/docs#plus-addressing ### Branding Files See https://www.kaimoku.tech/kuju-email/docs#branding-files ## Examples ---