[{"data":1,"prerenderedAt":865},["ShallowReactive",2],{"content-developer\u002Fself-hosting-dns-email":3,"surround-\u002Fdeveloper\u002Fself-hosting-dns-email":856},{"id":4,"title":5,"body":6,"description":849,"extension":850,"meta":851,"navigation":165,"path":852,"seo":853,"stem":854,"__hash__":855},"content\u002F3.developer\u002F32.self-hosting-dns-email.md","DNS & Email Setup",{"type":7,"value":8,"toc":832},"minimark",[9,13,21,26,31,34,45,49,65,71,74,78,84,90,94,97,103,106,112,116,119,123,200,204,211,248,255,261,268,272,279,300,310,387,390,507,511,517,523,573,585,589,592,622,625,631,635,641,644,690,693,697,700,820,828],[10,11,12],"p",{},"For production email delivery, your self-hosted Owlat instance needs proper DNS configuration. Without it, most email providers will reject or spam-folder your messages.",[14,15,18],"callout",{"title":16,"type":17},"Local development","info",[10,19,20],{},"DNS setup is only needed for production. For local testing, the MTA will attempt delivery with the default settings — most emails will be rejected by receiving servers, but you can verify the pipeline works end-to-end.",[22,23,25],"h2",{"id":24},"required-dns-records","Required DNS Records",[27,28,30],"h3",{"id":29},"a-record","A Record",[10,32,33],{},"Point your application domain to your server's IP address:",[35,36,41],"pre",{"className":37,"code":39,"language":40},[38],"language-text","owlat.example.com.    A    203.0.113.10\n","text",[42,43,39],"code",{"__ignoreMap":44},"",[27,46,48],{"id":47},"ptr-reverse-dns","PTR \u002F Reverse DNS",[10,50,51,52,55,56,59,60,64],{},"The ",[42,53,54],{},"EHLO_HOSTNAME"," in your ",[42,57,58],{},".env"," ",[61,62,63],"strong",{},"must"," have a PTR record matching your server's IP address. This is set through your hosting provider's control panel (not your DNS provider).",[35,66,69],{"className":67,"code":68,"language":40},[38],"# Your .env\nEHLO_HOSTNAME=mail.example.com\n\n# PTR record (set in hosting provider)\n203.0.113.10    PTR    mail.example.com\n",[42,70,68],{"__ignoreMap":44},[10,72,73],{},"Most receiving mail servers reject connections where the EHLO hostname doesn't match the PTR record.",[27,75,77],{"id":76},"mx-record-for-bounce-processing","MX Record for Bounce Processing",[10,79,51,80,83],{},[42,81,82],{},"RETURN_PATH_DOMAIN"," needs an MX record pointing to your server so bounce emails are routed back to the MTA on port 25:",[35,85,88],{"className":86,"code":87,"language":40},[38],"bounces.example.com.    MX    10 mail.example.com.\n",[42,89,87],{"__ignoreMap":44},[22,91,93],{"id":92},"spf","SPF",[10,95,96],{},"Add a TXT record on your sending domain authorizing your server's IP to send email:",[35,98,101],{"className":99,"code":100,"language":40},[38],"example.com.    TXT    \"v=spf1 ip4:203.0.113.10 -all\"\n",[42,102,100],{"__ignoreMap":44},[10,104,105],{},"If you use multiple IPs (separate transactional and campaign pools), include all of them:",[35,107,110],{"className":108,"code":109,"language":40},[38],"example.com.    TXT    \"v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 -all\"\n",[42,111,109],{"__ignoreMap":44},[22,113,115],{"id":114},"dkim","DKIM",[10,117,118],{},"DKIM signs outgoing emails with a cryptographic key, allowing receivers to verify the message hasn't been tampered with.",[27,120,122],{"id":121},"_1-generate-a-key-pair","1. Generate a Key Pair",[35,124,128],{"className":125,"code":126,"language":127,"meta":44,"style":44},"language-bash shiki shiki-themes github-light github-dark-dimmed","# Generate private key\nopenssl genrsa -out dkim-private.pem 2048\n\n# Extract public key\nopenssl rsa -in dkim-private.pem -pubout -outform PEM -out dkim-public.pem\n","bash",[42,129,130,139,160,167,173],{"__ignoreMap":44},[131,132,135],"span",{"class":133,"line":134},"line",1,[131,136,138],{"class":137},"sDN9O","# Generate private key\n",[131,140,142,146,150,154,157],{"class":133,"line":141},2,[131,143,145],{"class":144},"sOLd2","openssl",[131,147,149],{"class":148},"s-HuK"," genrsa",[131,151,153],{"class":152},"sviXB"," -out",[131,155,156],{"class":148}," dkim-private.pem",[131,158,159],{"class":152}," 2048\n",[131,161,163],{"class":133,"line":162},3,[131,164,166],{"emptyLinePlaceholder":165},true,"\n",[131,168,170],{"class":133,"line":169},4,[131,171,172],{"class":137},"# Extract public key\n",[131,174,176,178,181,184,186,189,192,195,197],{"class":133,"line":175},5,[131,177,145],{"class":144},[131,179,180],{"class":148}," rsa",[131,182,183],{"class":152}," -in",[131,185,156],{"class":148},[131,187,188],{"class":152}," -pubout",[131,190,191],{"class":152}," -outform",[131,193,194],{"class":148}," PEM",[131,196,153],{"class":152},[131,198,199],{"class":148}," dkim-public.pem\n",[27,201,203],{"id":202},"_2-add-the-dns-txt-record","2. Add the DNS TXT Record",[10,205,206,207,210],{},"Extract the base64 content from ",[42,208,209],{},"dkim-public.pem"," (strip the headers and join into one line):",[35,212,214],{"className":125,"code":213,"language":127,"meta":44,"style":44},"# Extract just the base64 content\ngrep -v '^-' dkim-public.pem | tr -d '\\n'\n",[42,215,216,221],{"__ignoreMap":44},[131,217,218],{"class":133,"line":134},[131,219,220],{"class":137},"# Extract just the base64 content\n",[131,222,223,226,229,232,235,239,242,245],{"class":133,"line":141},[131,224,225],{"class":144},"grep",[131,227,228],{"class":152}," -v",[131,230,231],{"class":148}," '^-'",[131,233,234],{"class":148}," dkim-public.pem",[131,236,238],{"class":237},"s7YZ4"," |",[131,240,241],{"class":144}," tr",[131,243,244],{"class":152}," -d",[131,246,247],{"class":148}," '\\n'\n",[10,249,250,251,254],{},"Create a TXT record at ",[42,252,253],{},"s1._domainkey.example.com",":",[35,256,259],{"className":257,"code":258,"language":40},[38],"s1._domainkey.example.com.    TXT    \"v=DKIM1; k=rsa; p=MIIBIjANBgkq...\"\n",[42,260,258],{"__ignoreMap":44},[14,262,265],{"title":263,"type":264},"DNS TXT record length","warning",[10,266,267],{},"If your public key is longer than 255 characters, you may need to split it across multiple quoted strings in the TXT record. Most DNS providers handle this automatically.",[27,269,271],{"id":270},"_3-configure-the-dkim_keys-environment-variable","3. Configure the DKIM_KEYS Environment Variable",[10,273,274,275,278],{},"Read the private key file and format it as a single-line string with ",[42,276,277],{},"\\n"," for newlines:",[35,280,282],{"className":125,"code":281,"language":127,"meta":44,"style":44},"# Convert private key to single-line format\nawk 'NF {sub(\u002F\\r\u002F, \"\"); printf \"%s\\\\n\",$0;}' dkim-private.pem\n",[42,283,284,289],{"__ignoreMap":44},[131,285,286],{"class":133,"line":134},[131,287,288],{"class":137},"# Convert private key to single-line format\n",[131,290,291,294,297],{"class":133,"line":141},[131,292,293],{"class":144},"awk",[131,295,296],{"class":148}," 'NF {sub(\u002F\\r\u002F, \"\"); printf \"%s\\\\n\",$0;}'",[131,298,299],{"class":148}," dkim-private.pem\n",[10,301,302,303,306,307,309],{},"Set the ",[42,304,305],{},"DKIM_KEYS"," variable in your ",[42,308,58],{}," as a JSON object:",[35,311,313],{"className":125,"code":312,"language":127,"meta":44,"style":44},"DKIM_KEYS={\"example.com\":{\"selector\":\"s1\",\"privateKey\":\"-----BEGIN RSA PRIVATE KEY-----\\nMIIEpAIBAAK...\\n-----END RSA PRIVATE KEY-----\\n\"}}\n",[42,314,315],{"__ignoreMap":44},[131,316,317,320,323,326,329,331,333,336,339,342,345,348,351,353,356,359,362,365,368,371,373,376,378,380,382,384],{"class":133,"line":134},[131,318,305],{"class":319},"sYgZi",[131,321,322],{"class":237},"=",[131,324,325],{"class":319},"{",[131,327,328],{"class":144},"\"example.com\"",[131,330,254],{"class":152},[131,332,325],{"class":148},[131,334,335],{"class":319},"\"",[131,337,338],{"class":144},"selector",[131,340,341],{"class":144},"\":\"",[131,343,344],{"class":144},"s1",[131,346,347],{"class":144},"\",\"",[131,349,350],{"class":144},"privateKey",[131,352,341],{"class":144},[131,354,355],{"class":144},"-----BEGIN",[131,357,358],{"class":148}," RSA",[131,360,361],{"class":148}," PRIVATE",[131,363,364],{"class":148}," KEY-----",[131,366,277],{"class":367},"s74oq",[131,369,370],{"class":148},"MIIEpAIBAAK...",[131,372,277],{"class":367},[131,374,375],{"class":148},"-----END",[131,377,358],{"class":148},[131,379,361],{"class":148},[131,381,364],{"class":148},[131,383,277],{"class":367},[131,385,386],{"class":148},"\"}}\n",[10,388,389],{},"The JSON format supports multiple domains:",[35,391,395],{"className":392,"code":393,"language":394,"meta":44,"style":44},"language-json shiki shiki-themes github-light github-dark-dimmed","{\n  \"example.com\": {\n    \"selector\": \"s1\",\n    \"privateKey\": \"-----BEGIN RSA PRIVATE KEY-----\\n...\\n-----END RSA PRIVATE KEY-----\\n\"\n  },\n  \"anotherdomain.com\": {\n    \"selector\": \"s1\",\n    \"privateKey\": \"-----BEGIN RSA PRIVATE KEY-----\\n...\\n-----END RSA PRIVATE KEY-----\\n\"\n  }\n}\n","json",[42,396,397,402,411,425,450,455,463,474,495,501],{"__ignoreMap":44},[131,398,399],{"class":133,"line":134},[131,400,401],{"class":319},"{\n",[131,403,404,408],{"class":133,"line":141},[131,405,407],{"class":406},"snmFh","  \"example.com\"",[131,409,410],{"class":319},": {\n",[131,412,413,416,419,422],{"class":133,"line":162},[131,414,415],{"class":406},"    \"selector\"",[131,417,418],{"class":319},": ",[131,420,421],{"class":148},"\"s1\"",[131,423,424],{"class":319},",\n",[131,426,427,430,432,435,437,440,442,445,447],{"class":133,"line":169},[131,428,429],{"class":406},"    \"privateKey\"",[131,431,418],{"class":319},[131,433,434],{"class":148},"\"-----BEGIN RSA PRIVATE KEY-----",[131,436,277],{"class":367},[131,438,439],{"class":148},"...",[131,441,277],{"class":367},[131,443,444],{"class":148},"-----END RSA PRIVATE KEY-----",[131,446,277],{"class":367},[131,448,449],{"class":148},"\"\n",[131,451,452],{"class":133,"line":175},[131,453,454],{"class":319},"  },\n",[131,456,458,461],{"class":133,"line":457},6,[131,459,460],{"class":406},"  \"anotherdomain.com\"",[131,462,410],{"class":319},[131,464,466,468,470,472],{"class":133,"line":465},7,[131,467,415],{"class":406},[131,469,418],{"class":319},[131,471,421],{"class":148},[131,473,424],{"class":319},[131,475,477,479,481,483,485,487,489,491,493],{"class":133,"line":476},8,[131,478,429],{"class":406},[131,480,418],{"class":319},[131,482,434],{"class":148},[131,484,277],{"class":367},[131,486,439],{"class":148},[131,488,277],{"class":367},[131,490,444],{"class":148},[131,492,277],{"class":367},[131,494,449],{"class":148},[131,496,498],{"class":133,"line":497},9,[131,499,500],{"class":319},"  }\n",[131,502,504],{"class":133,"line":503},10,[131,505,506],{"class":319},"}\n",[22,508,510],{"id":509},"dmarc","DMARC",[10,512,513,514,254],{},"DMARC tells receiving servers what to do when SPF or DKIM checks fail. Add a TXT record at ",[42,515,516],{},"_dmarc.example.com",[35,518,521],{"className":519,"code":520,"language":40},[38],"_dmarc.example.com.    TXT    \"v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com\"\n",[42,522,520],{"__ignoreMap":44},[524,525,526,539],"table",{},[527,528,529],"thead",{},[530,531,532,536],"tr",{},[533,534,535],"th",{},"Policy",[533,537,538],{},"Behavior",[540,541,542,553,563],"tbody",{},[530,543,544,550],{},[545,546,547],"td",{},[42,548,549],{},"p=none",[545,551,552],{},"Monitor only — no action on failures (good for initial setup)",[530,554,555,560],{},[545,556,557],{},[42,558,559],{},"p=quarantine",[545,561,562],{},"Send failing messages to spam folder",[530,564,565,570],{},[545,566,567],{},[42,568,569],{},"p=reject",[545,571,572],{},"Reject failing messages entirely (strictest)",[10,574,575,576,578,579,581,582,584],{},"Start with ",[42,577,549],{}," while verifying your setup, then move to ",[42,580,559],{}," or ",[42,583,569],{}," once everything is working.",[22,586,588],{"id":587},"ip-pools","IP Pools",[10,590,591],{},"The MTA supports separate IP pools for transactional and campaign emails. This protects your transactional email reputation from being affected by marketing campaigns.",[35,593,595],{"className":125,"code":594,"language":127,"meta":44,"style":44},"# In .env\nIP_POOLS_TRANSACTIONAL=203.0.113.10\nIP_POOLS_CAMPAIGN=203.0.113.11,203.0.113.12\n",[42,596,597,602,612],{"__ignoreMap":44},[131,598,599],{"class":133,"line":134},[131,600,601],{"class":137},"# In .env\n",[131,603,604,607,609],{"class":133,"line":141},[131,605,606],{"class":319},"IP_POOLS_TRANSACTIONAL",[131,608,322],{"class":237},[131,610,611],{"class":148},"203.0.113.10\n",[131,613,614,617,619],{"class":133,"line":162},[131,615,616],{"class":319},"IP_POOLS_CAMPAIGN",[131,618,322],{"class":237},[131,620,621],{"class":148},"203.0.113.11,203.0.113.12\n",[10,623,624],{},"Each IP in a pool needs its own PTR record and must be included in your SPF record.",[14,626,628],{"title":627,"type":17},"Single IP setup",[10,629,630],{},"If you only have one IP address, use it for both pools. Reputation separation won't apply, but everything will work correctly.",[22,632,634],{"id":633},"ip-warming","IP Warming",[14,636,638],{"title":637,"type":264},"New IPs have no reputation",[10,639,640],{},"Fresh IP addresses have no sending reputation. Sending large volumes immediately will trigger spam filters and potentially get your IP blocklisted. Gradually increase volume over 2-4 weeks.",[10,642,643],{},"A typical warming schedule for a new IP:",[524,645,646,656],{},[527,647,648],{},[530,649,650,653],{},[533,651,652],{},"Week",[533,654,655],{},"Daily Volume",[540,657,658,666,674,682],{},[530,659,660,663],{},[545,661,662],{},"1",[545,664,665],{},"50-100 emails",[530,667,668,671],{},[545,669,670],{},"2",[545,672,673],{},"500-1,000 emails",[530,675,676,679],{},[545,677,678],{},"3",[545,680,681],{},"5,000-10,000 emails",[530,683,684,687],{},[545,685,686],{},"4+",[545,688,689],{},"Full volume",[10,691,692],{},"Focus on sending to engaged recipients first (recent openers\u002Fclickers) to build positive reputation signals.",[22,694,696],{"id":695},"verification","Verification",[10,698,699],{},"Once your DNS records are configured:",[701,702,703,716,729],"ol",{},[704,705,706,709,710,715],"li",{},[61,707,708],{},"In-app verification"," — Owlat's domain verification flow checks SPF, DKIM, and DMARC records automatically. See ",[711,712,714],"a",{"href":713},"\u002Fguide\u002Fdeliverability","Deliverability"," for details.",[704,717,718,721,722,728],{},[61,719,720],{},"External testing"," — send a test email to ",[711,723,727],{"href":724,"rel":725},"https:\u002F\u002Fwww.mail-tester.com",[726],"nofollow","mail-tester.com"," and aim for a score of 9+\u002F10.",[704,730,731,254,734],{},[61,732,733],{},"Manual checks",[35,735,737],{"className":125,"code":736,"language":127,"meta":44,"style":44},"# Verify SPF\ndig TXT example.com +short\n\n# Verify DKIM\ndig TXT s1._domainkey.example.com +short\n\n# Verify DMARC\ndig TXT _dmarc.example.com +short\n\n# Verify PTR\ndig -x 203.0.113.10 +short\n",[42,738,739,744,758,762,767,778,782,787,798,802,807],{"__ignoreMap":44},[131,740,741],{"class":133,"line":134},[131,742,743],{"class":137},"# Verify SPF\n",[131,745,746,749,752,755],{"class":133,"line":141},[131,747,748],{"class":144},"dig",[131,750,751],{"class":148}," TXT",[131,753,754],{"class":148}," example.com",[131,756,757],{"class":148}," +short\n",[131,759,760],{"class":133,"line":162},[131,761,166],{"emptyLinePlaceholder":165},[131,763,764],{"class":133,"line":169},[131,765,766],{"class":137},"# Verify DKIM\n",[131,768,769,771,773,776],{"class":133,"line":175},[131,770,748],{"class":144},[131,772,751],{"class":148},[131,774,775],{"class":148}," s1._domainkey.example.com",[131,777,757],{"class":148},[131,779,780],{"class":133,"line":457},[131,781,166],{"emptyLinePlaceholder":165},[131,783,784],{"class":133,"line":465},[131,785,786],{"class":137},"# Verify DMARC\n",[131,788,789,791,793,796],{"class":133,"line":476},[131,790,748],{"class":144},[131,792,751],{"class":148},[131,794,795],{"class":148}," _dmarc.example.com",[131,797,757],{"class":148},[131,799,800],{"class":133,"line":497},[131,801,166],{"emptyLinePlaceholder":165},[131,803,804],{"class":133,"line":503},[131,805,806],{"class":137},"# Verify PTR\n",[131,808,810,812,815,818],{"class":133,"line":809},11,[131,811,748],{"class":144},[131,813,814],{"class":152}," -x",[131,816,817],{"class":152}," 203.0.113.10",[131,819,757],{"class":148},[10,821,822,823,827],{},"For details on the MTA architecture, rate limiting, and bounce processing, see ",[711,824,826],{"href":825},"\u002Fdeveloper\u002Fmta-system","MTA System",".",[829,830,831],"style",{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}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);}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s74oq, html code.shiki .s74oq{--shiki-default:#005CC5;--shiki-dark:#F47067}html pre.shiki code .snmFh, html code.shiki .snmFh{--shiki-default:#005CC5;--shiki-dark:#8DDB8C}",{"title":44,"searchDepth":141,"depth":141,"links":833},[834,839,840,845,846,847,848],{"id":24,"depth":141,"text":25,"children":835},[836,837,838],{"id":29,"depth":162,"text":30},{"id":47,"depth":162,"text":48},{"id":76,"depth":162,"text":77},{"id":92,"depth":141,"text":93},{"id":114,"depth":141,"text":115,"children":841},[842,843,844],{"id":121,"depth":162,"text":122},{"id":202,"depth":162,"text":203},{"id":270,"depth":162,"text":271},{"id":509,"depth":141,"text":510},{"id":587,"depth":141,"text":588},{"id":633,"depth":141,"text":634},{"id":695,"depth":141,"text":696},"Configure DNS records, DKIM signing, SPF, DMARC, and bounce handling for reliable email delivery.","md",{},"\u002Fdeveloper\u002Fself-hosting-dns-email",{"title":5,"description":849},"3.developer\u002F32.self-hosting-dns-email","3ho4Nq5ECM5pMzGz3uVppnFyLNx7ijBTv9s2Gkd2hQ0",[857,861],{"title":858,"path":859,"stem":860,"children":-1},"Self-Hosting Configuration","\u002Fdeveloper\u002Fself-hosting-config","3.developer\u002F31.self-hosting-config",{"title":862,"path":863,"stem":864,"children":-1},"Production Deployment","\u002Fdeveloper\u002Fself-hosting-production","3.developer\u002F33.self-hosting-production",1777110578604]