[{"data":1,"prerenderedAt":685},["ShallowReactive",2],{"navigation_docs":3,"-setup":32,"-setup-surround":683},[4,8,12,16,20,24,28],{"title":5,"path":6,"stem":7},"Set up your server","\u002Fsetup","1.setup",{"title":9,"path":10,"stem":11},"Addresses and domains","\u002Faddresses","2.addresses",{"title":13,"path":14,"stem":15},"Sign-in and discovery","\u002Fsign-in","3.sign-in",{"title":17,"path":18,"stem":19},"Mail apps","\u002Fproxy","4.proxy",{"title":21,"path":22,"stem":23},"Spam","\u002Fspam","5.spam",{"title":25,"path":26,"stem":27},"Configuration","\u002Fconfiguration","6.configuration",{"title":29,"path":30,"stem":31},"Troubleshooting","\u002Ftroubleshooting","7.troubleshooting",{"id":33,"title":34,"body":35,"description":134,"extension":677,"links":678,"meta":679,"navigation":680,"path":6,"seo":681,"stem":7,"__hash__":682},"docs\u002F1.setup.md","Set up your own Mailopus server",{"type":36,"value":37,"toc":660},"minimark",[38,42,45,50,94,98,105,110,124,128,144,153,162,168,177,186,217,223,232,301,306,313,320,328,331,360,373,376,385,391,395,398,416,419,443,450,457,460,464,480,483,487,514,520,537,543,552,556,570,583,587,593,598,607,615,623,626,630,656],[39,40,41],"p",{},"Mailopus runs as a Worker in your own Cloudflare account. Your mail sits in your D1 database and R2 bucket. The UI at app.mailopus.net is a static app that talks straight to your Worker from your browser. mailopus.net never receives your mail, never stores it and never holds a token for it.",[39,43,44],{},"This page takes you from nothing to a working inbox. Expect about 30 minutes, most of it waiting for DNS.",[46,47,49],"h2",{"id":48},"what-you-need","What you need",[51,52,53,72,78,84],"ul",{},[54,55,56,60,61,65,66,71],"li",{},[57,58,59],"strong",{},"A domain."," For example ",[62,63,64],"code",{},"example.com",". Its DNS has to be on Cloudflare, in the same Cloudflare account as the Worker, because Email Routing only hands mail to a Worker in the zone's own account. If the zone has to live in another account, see ",[67,68,70],"a",{"href":69},"\u002Faddresses#domains-in-another-cloudflare-account","Domains in another Cloudflare account",".",[54,73,74,77],{},[57,75,76],{},"A Cloudflare account on Workers Paid."," It costs $5 a month. Email Sending needs it, so the free plan can receive mail but can't send.",[54,79,80,83],{},[57,81,82],{},"Cloudflare, for now."," Mailopus runs only on Cloudflare today. Other hosts are planned.",[54,85,86,89,90,93],{},[57,87,88],{},"For a deploy from a clone."," Git, pnpm and Node.js. The helper scripts in ",[62,91,92],{},"scripts\u002F"," import TypeScript files directly, so use a Node.js version that strips types on its own, 22.18 or later.",[46,95,97],{"id":96},"_1-deploy-the-worker","1. Deploy the Worker",[39,99,100,101,104],{},"There are two ways. Both deploy ",[62,102,103],{},"wrangler.jsonc",", which is the template for your server.",[106,107,109],"h3",{"id":108},"with-the-deploy-to-cloudflare-button","With the Deploy to Cloudflare button",[39,111,112,113,119,120,123],{},"The button on the ",[67,114,118],{"href":115,"rel":116},"https:\u002F\u002Fgithub.com\u002Fmailopus\u002Fserver\u002Fblob\u002Fmain\u002FREADME.md",[117],"nofollow","README"," copies the repo into your GitHub and deploys it from there. Keep the build and deploy commands it fills in. The deploy command is ",[62,121,122],{},"pnpm run deploy",", and that applies the D1 migrations, which the button doesn't do on its own. Your claim code is in the first build's log, in Workers & Pages > mailopus > Deployments.",[106,125,127],{"id":126},"from-a-clone","From a clone",[129,130,135],"pre",{"className":131,"code":132,"language":133,"meta":134,"style":134},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm install\n","sh","",[62,136,137],{"__ignoreMap":134},[138,139,142],"span",{"class":140,"line":141},"line",1,[138,143,132],{},[129,145,147],{"className":131,"code":146,"language":133,"meta":134,"style":134},"npx wrangler login\n",[62,148,149],{"__ignoreMap":134},[138,150,151],{"class":140,"line":141},[138,152,146],{},[129,154,156],{"className":131,"code":155,"language":133,"meta":134,"style":134},"npx wrangler whoami\n",[62,157,158],{"__ignoreMap":134},[138,159,160],{"class":140,"line":141},[138,161,155],{},[39,163,164,167],{},[62,165,166],{},"whoami"," prints your account id. Then deploy:",[129,169,171],{"className":131,"code":170,"language":133,"meta":134,"style":134},"CLOUDFLARE_ACCOUNT_ID=\u003Cyour account id> pnpm run deploy\n",[62,172,173],{"__ignoreMap":134},[138,174,175],{"class":140,"line":141},[138,176,170],{},[39,178,179,181,182,185],{},[62,180,122],{}," runs ",[62,183,184],{},"scripts\u002Fdeploy.mjs",", which does these things in order:",[187,188,189,194,200,205,210],"ol",{},[54,190,191],{},[62,192,193],{},"nuxt build",[54,195,196,199],{},[62,197,198],{},"wrangler d1 migrations apply DB --remote",", only when the database already exists, so an update never runs new code on an old schema",[54,201,202],{},[62,203,204],{},"wrangler deploy",[54,206,207,209],{},[62,208,198],{}," again, so a first deploy gets its tables",[54,211,212,213,216],{},"if the Worker has no ",[62,214,215],{},"CLAIM_CODE"," secret yet, sets a random one and prints it with a claim link",[39,218,219,220,71],{},"Keep the claim code until step 5. Claiming your server needs it, so nobody who finds your new Worker can claim it before you. If you lose it before claiming, set a new one with ",[62,221,222],{},"npx wrangler secret put CLAIM_CODE",[39,224,225,226,228,229,231],{},"The first ",[62,227,204],{}," creates the resources ",[62,230,103],{}," names:",[233,234,235,248],"table",{},[236,237,238],"thead",{},[239,240,241,245],"tr",{},[242,243,244],"th",{},"Resource",[242,246,247],{},"Name",[249,250,251,262,271,281,291],"tbody",{},[239,252,253,257],{},[254,255,256],"td",{},"Worker",[254,258,259],{},[62,260,261],{},"mailopus",[239,263,264,267],{},[254,265,266],{},"D1 database",[254,268,269],{},[62,270,261],{},[239,272,273,276],{},[254,274,275],{},"R2 bucket",[254,277,278],{},[62,279,280],{},"mailopus-mail",[239,282,283,286],{},[254,284,285],{},"Queue",[254,287,288],{},[62,289,290],{},"mailopus-import",[239,292,293,296],{},[254,294,295],{},"Durable Object class",[254,297,298],{},[62,299,300],{},"Account",[39,302,303,305],{},[62,304,103],{}," leaves out resource ids on purpose. Wrangler creates the resources and fills them in.",[39,307,308,309,312],{},"When it's done the Worker answers at ",[62,310,311],{},"https:\u002F\u002Fmailopus.\u003Cyour subdomain>.workers.dev",". Write that URL down. You need it in the next step.",[39,314,315,316,319],{},"A custom domain works too, for example ",[62,317,318],{},"mail.example.com",". Add it to the Worker in the dashboard under Workers & Pages > mailopus > Settings > Domains & Routes, then use it everywhere this page says workers.dev.",[46,321,323,324,327],{"id":322},"_2-add-the-_mailopus-txt-record","2. Add the ",[62,325,326],{},"_mailopus"," TXT record",[39,329,330],{},"The UI finds your Worker through a TXT record on your domain. Add it in the Cloudflare dashboard under your domain's DNS > Records:",[233,332,333,345],{},[236,334,335],{},[239,336,337,340,342],{},[242,338,339],{},"Type",[242,341,247],{},[242,343,344],{},"Content",[249,346,347],{},[239,348,349,352,356],{},[254,350,351],{},"TXT",[254,353,354],{},[62,355,326],{},[254,357,358],{},[62,359,311],{},[39,361,362,363,366,367,370,371,71],{},"Cloudflare shows the full name as ",[62,364,365],{},"_mailopus.example.com",". The content is the Worker's origin: ",[62,368,369],{},"https:\u002F\u002F",", the host, and nothing after it. No path, no trailing slash. The UI ignores a record that isn't ",[62,372,369],{},[39,374,375],{},"Check it before you go on:",[129,377,379],{"className":131,"code":378,"language":133,"meta":134,"style":134},"dig +short TXT _mailopus.example.com\n",[62,380,381],{"__ignoreMap":134},[138,382,383],{"class":140,"line":141},[138,384,378],{},[39,386,387,388,390],{},"It should print the URL in quotes. ",[67,389,13],{"href":14}," explains what reads this record and when.",[46,392,394],{"id":393},"_3-turn-on-email-routing","3. Turn on Email Routing",[39,396,397],{},"Email Routing receives mail for your domain and hands it to the Worker.",[187,399,400,403,406],{},[54,401,402],{},"In the dashboard, open your domain, then Email > Email Routing.",[54,404,405],{},"Turn it on. Let it add its MX and SPF records. If the domain already receives mail somewhere else, those records replace that.",[54,407,408,409,412,413,415],{},"Under Routing rules, edit the catch-all address. Set the action to ",[57,410,411],{},"Send to a Worker"," and pick ",[62,414,261],{},". Save and make sure the rule is enabled.",[39,417,418],{},"The catch-all sends every address at the domain to the Worker, and the Worker decides what to accept. It looks up the recipient in its own list of routes in D1, in this order:",[187,420,421,427,437],{},[54,422,423,424],{},"the exact address, ",[62,425,426],{},"sam+news@example.com",[54,428,429,430,433,434],{},"the address without its ",[62,431,432],{},"+tag",", ",[62,435,436],{},"sam@example.com",[54,438,439,440],{},"a catch-all route for the domain, ",[62,441,442],{},"*@example.com",[39,444,445,446,449],{},"If none of them is there, the Worker refuses the message during the SMTP conversation with ",[62,447,448],{},"Address not found",", so the sender gets a bounce instead of a silent loss.",[39,451,452,453,71],{},"Claiming in step 5 routes only the address you claim with. If you want every address at the domain to reach you, turn on the catch-all afterwards, as in ",[67,454,456],{"href":455},"\u002Faddresses#the-catch-all-route","The catch-all route",[39,458,459],{},"You can route only some addresses in Email Routing instead of the catch-all. Mail to any address without a rule then never reaches Mailopus, and Email Routing handles it by its own rules.",[46,461,463],{"id":462},"_4-turn-on-email-sending","4. Turn on Email Sending",[187,465,466,469,474,477],{},[54,467,468],{},"Open Email > Email Sending.",[54,470,471,472,71],{},"Add ",[62,473,64],{},[54,475,476],{},"Add every DNS record it lists. With the zone on Cloudflare it can add them for you.",[54,478,479],{},"Wait until the domain shows as verified.",[39,481,482],{},"Until then, every send fails. Mailopus only sends From an address that is one of your identities in D1, so even with Email Sending on, nobody can use your Worker to send as an address you haven't added.",[46,484,486],{"id":485},"_5-claim-your-server","5. Claim your server",[187,488,489,502,507],{},[54,490,491,492,495,496,501],{},"Open the claim link the deploy printed, ",[62,493,494],{},"https:\u002F\u002Fapp.mailopus.net\u002Fauth\u002Fclaim#code=...",". It fills in your claim code. You can also open ",[67,497,500],{"href":498,"rel":499},"https:\u002F\u002Fapp.mailopus.net\u002Fauth\u002Fclaim",[117],"app.mailopus.net\u002Fauth\u002Fclaim"," and paste the code. The code stays in the link's fragment, which browsers never send to a server.",[54,503,504,505,71],{},"Enter your name and an address at your domain, for example ",[62,506,436],{},[54,508,509,510,513],{},"Press ",[57,511,512],{},"Create passkey"," and save the passkey where your browser offers, such as iCloud Keychain, Google Password Manager or a security key.",[39,515,516,517,519],{},"The page looks up ",[62,518,365],{},", finds your Worker and registers the passkey there. Claiming creates, in one transaction:",[51,521,522,525,528,531,534],{},[54,523,524],{},"you, as the owner and admin",[54,526,527],{},"your mail account and its standard mailboxes: Inbox, Drafts, Sent, Archive, Junk, Trash and Scheduled",[54,529,530],{},"a send-as identity for the address you entered",[54,532,533],{},"a route for the address you entered",[54,535,536],{},"your passkey",[39,538,539,540,542],{},"Nothing is saved until the passkey verifies, so an abandoned claim leaves the server open to whoever has the claim code. Once one claim succeeds, the claim page refuses everyone else for good, code or not. A server with no ",[62,541,215],{}," set refuses every claim.",[39,544,545,546,551],{},"From then on, sign in at ",[67,547,550],{"href":548,"rel":549},"https:\u002F\u002Fapp.mailopus.net",[117],"app.mailopus.net"," with your address and that passkey.",[46,553,555],{"id":554},"_6-check-it-works","6. Check it works",[187,557,558,564,567],{},[54,559,560,561,563],{},"Send a message from another account to ",[62,562,436],{},". It should show up in your Inbox within a few seconds.",[54,565,566],{},"Send one from Mailopus to an outside address. If it fails, Email Sending isn't verified yet.",[54,568,569],{},"Send one to an address you never added. The sender should get a bounce.",[39,571,572,575,576,579,580,71],{},[62,573,574],{},"npx wrangler tail"," streams your Worker's logs. Each received message logs ",[62,577,578],{},"email: stored",", and each refused one logs ",[62,581,582],{},"email: no account for recipient",[46,584,586],{"id":585},"updates","Updates",[39,588,589,592],{},[57,590,591],{},"From the Deploy button."," Sync your GitHub copy with the upstream repo. Each push redeploys and migrates.",[39,594,595],{},[57,596,597],{},"From a clone.",[129,599,601],{"className":131,"code":600,"language":133,"meta":134,"style":134},"git pull\n",[62,602,603],{"__ignoreMap":134},[138,604,605],{"class":140,"line":141},[138,606,600],{},[129,608,609],{"className":131,"code":132,"language":133,"meta":134,"style":134},[62,610,611],{"__ignoreMap":134},[138,612,613],{"class":140,"line":141},[138,614,132],{},[129,616,617],{"className":131,"code":170,"language":133,"meta":134,"style":134},[62,618,619],{"__ignoreMap":134},[138,620,621],{"class":140,"line":141},[138,622,170],{},[39,624,625],{},"Migrations run before and after the deploy, as in step 1.",[46,627,629],{"id":628},"next","Next",[51,631,632,641,646,651],{},[54,633,634,636,637,640],{},[67,635,9],{"href":10},": more addresses, ",[62,638,639],{},"+"," aliases, more domains",[54,642,643,645],{},[67,644,17],{"href":18},": Apple Mail, Thunderbird and Outlook through the IMAP and SMTP proxy",[54,647,648,650],{},[67,649,21],{"href":22},": what your server filters on its own, and how to add rules",[54,652,653,655],{},[67,654,25],{"href":26},": every setting, notifications, Gmail import and backups",[657,658,659],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":134,"searchDepth":661,"depth":661,"links":662},2,[663,664,669,671,672,673,674,675,676],{"id":48,"depth":661,"text":49},{"id":96,"depth":661,"text":97,"children":665},[666,668],{"id":108,"depth":667,"text":109},3,{"id":126,"depth":667,"text":127},{"id":322,"depth":661,"text":670},"2. Add the _mailopus TXT record",{"id":393,"depth":661,"text":394},{"id":462,"depth":661,"text":463},{"id":485,"depth":661,"text":486},{"id":554,"depth":661,"text":555},{"id":585,"depth":661,"text":586},{"id":628,"depth":661,"text":629},"md",null,{},{"title":5},{"description":41,"title":34},"iF5J1aOV72437d11UvVa92eysUsPPQWJjZE71JD46lI",[678,684],{"title":9,"path":10,"stem":11,"description":134,"children":-1},1794228095132]