[{"data":1,"prerenderedAt":3440},["ShallowReactive",2],{"navigation_docs_en":3,"-en-storage-mail-mail":342,"-en-storage-mail-mail-surround":3435},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":252,"body":344,"description":3428,"extension":3429,"links":3430,"meta":3431,"navigation":3432,"path":253,"seo":3433,"stem":254,"__hash__":3434},"docs_en\u002Fen\u002F8.storage-mail\u002F2.mail.md",{"type":345,"value":346,"toc":3409},"minimark",[347,365,370,381,414,418,517,534,538,544,651,654,838,842,858,1081,1091,1102,1106,1122,1150,1241,1244,1264,1289,1293,1296,1420,1439,1483,1494,1541,1703,1709,1752,1756,1828,1862,1866,1888,2164,2178,2275,2286,2290,2600,2609,2613,2625,2631,2640,2679,2728,2732,2735,2784,3109,3285,3304,3370,3385,3405],[348,349,350,351,355,356,360,361,364],"p",{},"In the ",[352,353,354],"a",{"href":41},"shop sample app",", the ",[357,358,359],"code",{},"sendOrderConfirmation"," listener runs in the worker and sends an order confirmation rendered from MJML. Business code (the listener) sees only the ",[357,362,363],{},"mail.Mailer"," interface; whether SMTP, log output, or a test memory mailer sits behind it is decided by the wiring.",[366,367,369],"h2",{"id":368},"the-second-deliberate-abstraction","The second deliberate abstraction",[348,371,372,373,376,377,380],{},"The kit's design principle is ",[352,374,375],{"href":69},"\"centralize, don't abstract\"",": mail is, next to ",[352,378,379],{"href":249},"storage",", the other exception where the interface is deliberate:",[382,383,384,407],"ul",{},[385,386,387,388,391,392,398,399,402,403,406],"li",{},"The actual sending is done by the ",[357,389,390],{},"mail\u002Fsmtp"," subpackage (via ",[352,393,397],{"href":394,"rel":395},"https:\u002F\u002Fgithub.com\u002Fwneessen\u002Fgo-mail",[396],"nofollow","wneessen\u002Fgo-mail","), the MJML compilation by ",[357,400,401],{},"mail\u002Fmjml",". The ",[357,404,405],{},"mail"," root package imports neither. Your service layer compiles without an SMTP client or a WASM runtime.",[385,408,409,410,413],{},"In tests, ",[357,411,412],{},"mail.NewMemory()"," replaces the driver in one line, so sent messages can be asserted on without a real SMTP connection.",[366,415,417],{"id":416},"the-interface","The interface",[419,420,425],"pre",{"className":421,"code":422,"language":423,"meta":424,"style":424},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fmail\"\n\ntype Mailer interface {\n    Send(ctx context.Context, msg *Message) error\n}\n","go","",[357,426,427,447,454,469,511],{"__ignoreMap":424},[428,429,432,436,440,444],"span",{"class":430,"line":431},"line",1,[428,433,435],{"class":434},"s7zQu","import",[428,437,439],{"class":438},"sMK4o"," \"",[428,441,443],{"class":442},"sBMFI","github.com\u002Fgp-system\u002Fgpsystem\u002Fmail",[428,445,446],{"class":438},"\"\n",[428,448,450],{"class":430,"line":449},2,[428,451,453],{"emptyLinePlaceholder":452},true,"\n",[428,455,457,460,463,466],{"class":430,"line":456},3,[428,458,459],{"class":438},"type",[428,461,462],{"class":442}," Mailer",[428,464,465],{"class":438}," interface",[428,467,468],{"class":438}," {\n",[428,470,472,476,479,483,486,489,492,495,498,501,504,507],{"class":430,"line":471},4,[428,473,475],{"class":474},"s2Zo4","    Send",[428,477,478],{"class":438},"(",[428,480,482],{"class":481},"sHdIc","ctx",[428,484,485],{"class":442}," context",[428,487,488],{"class":438},".",[428,490,491],{"class":442},"Context",[428,493,494],{"class":438},",",[428,496,497],{"class":481}," msg",[428,499,500],{"class":438}," *",[428,502,503],{"class":442},"Message",[428,505,506],{"class":438},")",[428,508,510],{"class":509},"spNyl"," error\n",[428,512,514],{"class":430,"line":513},5,[428,515,516],{"class":438},"}\n",[348,518,519,522,523,526,527,530,531,488],{},[357,520,521],{},"Send"," is synchronous and calls ",[357,524,525],{},"Validate()"," first: no recipient returns ",[357,528,529],{},"ErrNoRecipient",", no body returns ",[357,532,533],{},"ErrNoBody",[366,535,537],{"id":536},"building-a-message","Building a Message",[348,539,540,541,543],{},"You build a ",[357,542,503],{}," fluently; construction never fails outright, omissions surface at send time:",[419,545,547],{"className":421,"code":546,"language":423,"meta":424,"style":424},"msg := mail.NewMessage().\n    WithFromNamed(\"Shop\", \"noreply@acme.test\").  \u002F\u002F optional: the driver's default sender steps in\n    WithTo(\"user@example.com\").\n    WithSubject(\"Your order confirmation\").\n    WithText(\"Thanks for your order!\")\n",[357,548,549,569,601,618,634],{"__ignoreMap":424},[428,550,551,555,558,561,563,566],{"class":430,"line":431},[428,552,554],{"class":553},"sTEyZ","msg ",[428,556,557],{"class":438},":=",[428,559,560],{"class":553}," mail",[428,562,488],{"class":438},[428,564,565],{"class":474},"NewMessage",[428,567,568],{"class":438},"().\n",[428,570,571,574,576,579,583,585,587,589,592,594,597],{"class":430,"line":449},[428,572,573],{"class":474},"    WithFromNamed",[428,575,478],{"class":438},[428,577,578],{"class":438},"\"",[428,580,582],{"class":581},"sfazB","Shop",[428,584,578],{"class":438},[428,586,494],{"class":438},[428,588,439],{"class":438},[428,590,591],{"class":581},"noreply@acme.test",[428,593,578],{"class":438},[428,595,596],{"class":438},").",[428,598,600],{"class":599},"sHwdD","  \u002F\u002F optional: the driver's default sender steps in\n",[428,602,603,606,608,610,613,615],{"class":430,"line":456},[428,604,605],{"class":474},"    WithTo",[428,607,478],{"class":438},[428,609,578],{"class":438},[428,611,612],{"class":581},"user@example.com",[428,614,578],{"class":438},[428,616,617],{"class":438},").\n",[428,619,620,623,625,627,630,632],{"class":430,"line":471},[428,621,622],{"class":474},"    WithSubject",[428,624,478],{"class":438},[428,626,578],{"class":438},[428,628,629],{"class":581},"Your order confirmation",[428,631,578],{"class":438},[428,633,617],{"class":438},[428,635,636,639,641,643,646,648],{"class":430,"line":513},[428,637,638],{"class":474},"    WithText",[428,640,478],{"class":438},[428,642,578],{"class":438},[428,644,645],{"class":581},"Thanks for your order!",[428,647,578],{"class":438},[428,649,650],{"class":438},")\n",[348,652,653],{},"The full builder surface:",[655,656,657,670],"table",{},[658,659,660],"thead",{},[661,662,663,667],"tr",{},[664,665,666],"th",{},"Method",[664,668,669],{},"What it does",[671,672,673,696,709,722,735,745,755,768,787,797,811,825],"tbody",{},[661,674,675,685],{},[676,677,678,681,682],"td",{},[357,679,680],{},"WithFrom(addr)"," \u002F ",[357,683,684],{},"WithFromNamed(name, addr)",[676,686,687,688,691,692,695],{},"sender; when omitted, the driver's configured default (",[357,689,690],{},"MAIL_FROM_ADDRESS","\u002F",[357,693,694],{},"MAIL_FROM_NAME",") applies",[661,697,698,706],{},[676,699,700,681,703],{},[357,701,702],{},"WithTo(addrs...)",[357,704,705],{},"WithToNamed(name, addr)",[676,707,708],{},"To recipient(s)",[661,710,711,719],{},[676,712,713,681,716],{},[357,714,715],{},"WithCc(addrs...)",[357,717,718],{},"WithCcNamed(name, addr)",[676,720,721],{},"Cc recipient(s)",[661,723,724,732],{},[676,725,726,681,729],{},[357,727,728],{},"WithBcc(addrs...)",[357,730,731],{},"WithBccNamed(name, addr)",[676,733,734],{},"Bcc recipient(s)",[661,736,737,742],{},[676,738,739],{},[357,740,741],{},"WithReplyTo(addr)",[676,743,744],{},"Reply-To",[661,746,747,752],{},[676,748,749],{},[357,750,751],{},"WithSubject(s)",[676,753,754],{},"subject",[661,756,757,762],{},[676,758,759],{},[357,760,761],{},"WithHeader(key, value)",[676,763,764,765,506],{},"custom header (e.g. ",[357,766,767],{},"List-Unsubscribe",[661,769,770,778],{},[676,771,772,681,775],{},[357,773,774],{},"WithText(s)",[357,776,777],{},"WithHTML(s)",[676,779,780,781,681,784],{},"shorthand for ",[357,782,783],{},"WithBody(mail.Text(s))",[357,785,786],{},"WithBody(mail.HTML(s))",[661,788,789,794],{},[676,790,791],{},[357,792,793],{},"WithBody(b mail.Body)",[676,795,796],{},"any body (the last call wins)",[661,798,799,804],{},[676,800,801],{},[357,802,803],{},"WithAttachment(filename, data, contentType)",[676,805,806,807,810],{},"attachment from memory (empty ",[357,808,809],{},"contentType",": the driver sniffs it)",[661,812,813,818],{},[676,814,815],{},[357,816,817],{},"WithAttachmentReader(filename, r, contentType)",[676,819,820,821,824],{},"streamed attachment (",[357,822,823],{},"r"," is read once, at send time)",[661,826,827,832],{},[676,828,829],{},[357,830,831],{},"WithEmbed(filename, data, contentType)",[676,833,834,835],{},"inline resource, referenced from HTML as ",[357,836,837],{},"cid:\u003Cfilename>",[366,839,841],{"id":840},"body-types","Body types",[348,843,844,845,848,849,852,853,857],{},"Every body implements the ",[357,846,847],{},"mail.Body"," interface (",[357,850,851],{},"Render(ctx) (Content, error)","; drivers render ",[854,855,856],"strong",{},"inside"," the send span, so template errors are traced too):",[419,859,861],{"className":421,"code":860,"language":423,"meta":424,"style":424},"\u002F\u002F 1. plain text\nmsg.WithText(\"Your account is ready.\")\n\n\u002F\u002F 2. raw HTML, with an optional text alternative\nmsg.WithBody(mail.HTML(\"\u003Cp>Your account is ready.\u003C\u002Fp>\").WithText(\"Your account is ready.\"))\n\n\u002F\u002F 3. html\u002Ftemplate + text\u002Ftemplate alternative from the same fs.FS\n\u002F\u002Fgo:embed templates\nvar tmpls embed.FS\n\nmsg.WithBody(mail.Template(tmpls, \"templates\u002Finvoice.html.tmpl\", inv).\n    WithTextTemplate(\"templates\u002Finvoice.txt.tmpl\"))\n\n\u002F\u002F 4. pre-parsed templates, for consumers with their own cache\nmsg.WithBody(mail.Parsed(htmlTpl, textTpl, data))\n",[357,862,863,868,889,893,898,940,945,951,957,974,979,1018,1035,1040,1046],{"__ignoreMap":424},[428,864,865],{"class":430,"line":431},[428,866,867],{"class":599},"\u002F\u002F 1. plain text\n",[428,869,870,873,875,878,880,882,885,887],{"class":430,"line":449},[428,871,872],{"class":553},"msg",[428,874,488],{"class":438},[428,876,877],{"class":474},"WithText",[428,879,478],{"class":438},[428,881,578],{"class":438},[428,883,884],{"class":581},"Your account is ready.",[428,886,578],{"class":438},[428,888,650],{"class":438},[428,890,891],{"class":430,"line":456},[428,892,453],{"emptyLinePlaceholder":452},[428,894,895],{"class":430,"line":471},[428,896,897],{"class":599},"\u002F\u002F 2. raw HTML, with an optional text alternative\n",[428,899,900,902,904,907,909,911,913,916,918,920,923,925,927,929,931,933,935,937],{"class":430,"line":513},[428,901,872],{"class":553},[428,903,488],{"class":438},[428,905,906],{"class":474},"WithBody",[428,908,478],{"class":438},[428,910,405],{"class":553},[428,912,488],{"class":438},[428,914,915],{"class":474},"HTML",[428,917,478],{"class":438},[428,919,578],{"class":438},[428,921,922],{"class":581},"\u003Cp>Your account is ready.\u003C\u002Fp>",[428,924,578],{"class":438},[428,926,596],{"class":438},[428,928,877],{"class":474},[428,930,478],{"class":438},[428,932,578],{"class":438},[428,934,884],{"class":581},[428,936,578],{"class":438},[428,938,939],{"class":438},"))\n",[428,941,943],{"class":430,"line":942},6,[428,944,453],{"emptyLinePlaceholder":452},[428,946,948],{"class":430,"line":947},7,[428,949,950],{"class":599},"\u002F\u002F 3. html\u002Ftemplate + text\u002Ftemplate alternative from the same fs.FS\n",[428,952,954],{"class":430,"line":953},8,[428,955,956],{"class":599},"\u002F\u002Fgo:embed templates\n",[428,958,960,963,966,969,971],{"class":430,"line":959},9,[428,961,962],{"class":438},"var",[428,964,965],{"class":553}," tmpls ",[428,967,968],{"class":442},"embed",[428,970,488],{"class":438},[428,972,973],{"class":442},"FS\n",[428,975,977],{"class":430,"line":976},10,[428,978,453],{"emptyLinePlaceholder":452},[428,980,982,984,986,988,990,992,994,997,999,1002,1004,1006,1009,1011,1013,1016],{"class":430,"line":981},11,[428,983,872],{"class":553},[428,985,488],{"class":438},[428,987,906],{"class":474},[428,989,478],{"class":438},[428,991,405],{"class":553},[428,993,488],{"class":438},[428,995,996],{"class":474},"Template",[428,998,478],{"class":438},[428,1000,1001],{"class":553},"tmpls",[428,1003,494],{"class":438},[428,1005,439],{"class":438},[428,1007,1008],{"class":581},"templates\u002Finvoice.html.tmpl",[428,1010,578],{"class":438},[428,1012,494],{"class":438},[428,1014,1015],{"class":553}," inv",[428,1017,617],{"class":438},[428,1019,1021,1024,1026,1028,1031,1033],{"class":430,"line":1020},12,[428,1022,1023],{"class":474},"    WithTextTemplate",[428,1025,478],{"class":438},[428,1027,578],{"class":438},[428,1029,1030],{"class":581},"templates\u002Finvoice.txt.tmpl",[428,1032,578],{"class":438},[428,1034,939],{"class":438},[428,1036,1038],{"class":430,"line":1037},13,[428,1039,453],{"emptyLinePlaceholder":452},[428,1041,1043],{"class":430,"line":1042},14,[428,1044,1045],{"class":599},"\u002F\u002F 4. pre-parsed templates, for consumers with their own cache\n",[428,1047,1049,1051,1053,1055,1057,1059,1061,1064,1066,1069,1071,1074,1076,1079],{"class":430,"line":1048},15,[428,1050,872],{"class":553},[428,1052,488],{"class":438},[428,1054,906],{"class":474},[428,1056,478],{"class":438},[428,1058,405],{"class":553},[428,1060,488],{"class":438},[428,1062,1063],{"class":474},"Parsed",[428,1065,478],{"class":438},[428,1067,1068],{"class":553},"htmlTpl",[428,1070,494],{"class":438},[428,1072,1073],{"class":553}," textTpl",[428,1075,494],{"class":438},[428,1077,1078],{"class":553}," data",[428,1080,939],{"class":438},[348,1082,1083,1086,1087,1090],{},[357,1084,1085],{},"mail.Template"," can be extended with ",[357,1088,1089],{},"WithFuncs(template.FuncMap)"," before parsing.",[1092,1093,1094,1095,691,1098,1101],"note",{},"There is no automatic text\u002Fplain derivation from HTML or MJML: when you need a text alternative, set it explicitly with ",[357,1096,1097],{},".WithText()",[357,1099,1100],{},".WithTextTemplate()",", and an HTML-only message stays HTML-only.",[366,1103,1105],{"id":1104},"mjml","MJML",[419,1107,1109],{"className":421,"code":1108,"language":423,"meta":424,"style":424},"import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fmail\u002Fmjml\"\n",[357,1110,1111],{"__ignoreMap":424},[428,1112,1113,1115,1117,1120],{"class":430,"line":431},[428,1114,435],{"class":434},[428,1116,439],{"class":438},[428,1118,1119],{"class":442},"github.com\u002Fgp-system\u002Fgpsystem\u002Fmail\u002Fmjml",[428,1121,446],{"class":438},[348,1123,1124,1125,1127,1128,1130,1131,1134,1135,1139,1140,1145,1146,1149],{},"The ",[357,1126,1104],{}," package is a ",[357,1129,847],{}," too: the source is first rendered as an ",[854,1132,1133],{},"html\u002Ftemplate"," (interpolated data is escaped), then compiled from ",[352,1136,1105],{"href":1137,"rel":1138},"https:\u002F\u002Fmjml.io\u002F",[396]," into responsive, email-client-friendly HTML. It runs in pure Go (",[352,1141,1144],{"href":1142,"rel":1143},"https:\u002F\u002Fgithub.com\u002FBoostport\u002Fmjml-go",[396],"Boostport\u002Fmjml-go",", wazero WASM runtime). ",[854,1147,1148],{},"No Node.js needed"," for build or runtime.",[419,1151,1153],{"className":421,"code":1152,"language":423,"meta":424,"style":424},"body := mjml.Template(tmpls, \"templates\u002Forder_confirmation.mjml.tmpl\", data).\n    WithTextTemplate(\"templates\u002Forder_confirmation.txt.tmpl\")\n\n\u002F\u002F or from an inline source:\nbody := mjml.String(`\u003Cmjml>\u003Cmj-body>...\u003C\u002Fmj-body>\u003C\u002Fmjml>`, data)\n",[357,1154,1155,1188,1203,1207,1212],{"__ignoreMap":424},[428,1156,1157,1160,1162,1165,1167,1169,1171,1173,1175,1177,1180,1182,1184,1186],{"class":430,"line":431},[428,1158,1159],{"class":553},"body ",[428,1161,557],{"class":438},[428,1163,1164],{"class":553}," mjml",[428,1166,488],{"class":438},[428,1168,996],{"class":474},[428,1170,478],{"class":438},[428,1172,1001],{"class":553},[428,1174,494],{"class":438},[428,1176,439],{"class":438},[428,1178,1179],{"class":581},"templates\u002Forder_confirmation.mjml.tmpl",[428,1181,578],{"class":438},[428,1183,494],{"class":438},[428,1185,1078],{"class":553},[428,1187,617],{"class":438},[428,1189,1190,1192,1194,1196,1199,1201],{"class":430,"line":449},[428,1191,1023],{"class":474},[428,1193,478],{"class":438},[428,1195,578],{"class":438},[428,1197,1198],{"class":581},"templates\u002Forder_confirmation.txt.tmpl",[428,1200,578],{"class":438},[428,1202,650],{"class":438},[428,1204,1205],{"class":430,"line":456},[428,1206,453],{"emptyLinePlaceholder":452},[428,1208,1209],{"class":430,"line":471},[428,1210,1211],{"class":599},"\u002F\u002F or from an inline source:\n",[428,1213,1214,1216,1218,1220,1222,1225,1227,1230,1233,1235,1237,1239],{"class":430,"line":513},[428,1215,1159],{"class":553},[428,1217,557],{"class":438},[428,1219,1164],{"class":553},[428,1221,488],{"class":438},[428,1223,1224],{"class":474},"String",[428,1226,478],{"class":438},[428,1228,1229],{"class":438},"`",[428,1231,1232],{"class":581},"\u003Cmjml>\u003Cmj-body>...\u003C\u002Fmj-body>\u003C\u002Fmjml>",[428,1234,1229],{"class":438},[428,1236,494],{"class":438},[428,1238,1078],{"class":553},[428,1240,650],{"class":438},[348,1242,1243],{},"Options:",[382,1245,1246,1255],{},[385,1247,1248,1251,1252,488],{},[357,1249,1250],{},"mjml.Minify(bool)",": minify the compiled HTML; default ",[357,1253,1254],{},"true",[385,1256,1257,1260,1261,488],{},[357,1258,1259],{},"mjml.ValidationLevel(\"strict\" | \"soft\" | \"skip\")",": MJML validation strictness; default ",[357,1262,1263],{},"soft",[1265,1266,1267,1268,1270,1271,1274,1275,1277,1278,1281,1282,1284,1285,1288],"warning",{},"Importing ",[357,1269,401],{}," links a WASM runtime into the binary, and the ",[854,1272,1273],{},"first render"," pays for loading the WASM module. Subsequent calls are cheaper. Only the binary that actually imports the package carries that cost: the ",[357,1276,405],{}," root and the ",[357,1279,1280],{},"smtp"," driver know nothing about ",[357,1283,1104],{},". Typically only ",[357,1286,1287],{},"cmd\u002Fworker"," imports it.",[366,1290,1292],{"id":1291},"drivers","Drivers",[1294,1295,1280],"h3",{"id":1280},[419,1297,1299],{"className":421,"code":1298,"language":423,"meta":424,"style":424},"import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fmail\u002Fsmtp\"\n\nmailer, err := smtp.New(ctx, cfg.Mail)              \u002F\u002F errors on empty Host or FromAddress\nmailer, err := smtp.NewIfConfigured(ctx, cfg.Mail)   \u002F\u002F empty Host -> mail.NewDiscard(), no error\nmailer := smtp.MustNewIfConfigured(ctx, cfg.Mail)    \u002F\u002F panics on error; the usual main() constructor\n",[357,1300,1301,1312,1316,1355,1389],{"__ignoreMap":424},[428,1302,1303,1305,1307,1310],{"class":430,"line":431},[428,1304,435],{"class":434},[428,1306,439],{"class":438},[428,1308,1309],{"class":442},"github.com\u002Fgp-system\u002Fgpsystem\u002Fmail\u002Fsmtp",[428,1311,446],{"class":438},[428,1313,1314],{"class":430,"line":449},[428,1315,453],{"emptyLinePlaceholder":452},[428,1317,1318,1321,1323,1326,1328,1331,1333,1336,1338,1340,1342,1345,1347,1350,1352],{"class":430,"line":456},[428,1319,1320],{"class":553},"mailer",[428,1322,494],{"class":438},[428,1324,1325],{"class":553}," err ",[428,1327,557],{"class":438},[428,1329,1330],{"class":553}," smtp",[428,1332,488],{"class":438},[428,1334,1335],{"class":474},"New",[428,1337,478],{"class":438},[428,1339,482],{"class":553},[428,1341,494],{"class":438},[428,1343,1344],{"class":553}," cfg",[428,1346,488],{"class":438},[428,1348,1349],{"class":553},"Mail",[428,1351,506],{"class":438},[428,1353,1354],{"class":599},"              \u002F\u002F errors on empty Host or FromAddress\n",[428,1356,1357,1359,1361,1363,1365,1367,1369,1372,1374,1376,1378,1380,1382,1384,1386],{"class":430,"line":471},[428,1358,1320],{"class":553},[428,1360,494],{"class":438},[428,1362,1325],{"class":553},[428,1364,557],{"class":438},[428,1366,1330],{"class":553},[428,1368,488],{"class":438},[428,1370,1371],{"class":474},"NewIfConfigured",[428,1373,478],{"class":438},[428,1375,482],{"class":553},[428,1377,494],{"class":438},[428,1379,1344],{"class":553},[428,1381,488],{"class":438},[428,1383,1349],{"class":553},[428,1385,506],{"class":438},[428,1387,1388],{"class":599},"   \u002F\u002F empty Host -> mail.NewDiscard(), no error\n",[428,1390,1391,1394,1396,1398,1400,1403,1405,1407,1409,1411,1413,1415,1417],{"class":430,"line":513},[428,1392,1393],{"class":553},"mailer ",[428,1395,557],{"class":438},[428,1397,1330],{"class":553},[428,1399,488],{"class":438},[428,1401,1402],{"class":474},"MustNewIfConfigured",[428,1404,478],{"class":438},[428,1406,482],{"class":553},[428,1408,494],{"class":438},[428,1410,1344],{"class":553},[428,1412,488],{"class":438},[428,1414,1349],{"class":553},[428,1416,506],{"class":438},[428,1418,1419],{"class":599},"    \u002F\u002F panics on error; the usual main() constructor\n",[348,1421,1422,1424,1425,691,1428,1431,1432,1434,1435,1438],{},[357,1423,1335],{}," verifies the connection with a dial+close at construction, the same convention as ",[357,1426,1427],{},"pg.NewPool",[357,1429,1430],{},"queue.NewClient","'s ping: a broken config fails at startup, not at the first email. Every ",[357,1433,521],{}," opens its own OTel span (",[357,1436,1437],{},"smtp.send",") with a recipient count; never with addresses.",[348,1440,1441,1444,1445,1448,1449,1452,1453,1455,1456,1458,1459,1461,1462,1465,1466,1468,1469,1471,1472,1474,1475,1477,1478,1482],{},[357,1442,1443],{},"Host"," and ",[357,1446,1447],{},"FromAddress"," are not ",[357,1450,1451],{},"required"," env tags: an empty ",[357,1454,1443],{}," is how a project signals \"no mail transport configured\" (usually in dev, or in a service that never actually sends mail). ",[357,1457,1371],{}," (and its panicking counterpart ",[357,1460,1402],{},") turns that into ",[357,1463,1464],{},"mail.NewDiscard()"," (see below) instead of an error; ",[357,1467,1335],{}," still errors on an empty ",[357,1470,1447],{}," once ",[357,1473,1443],{}," is set, since that would silently mis-address every message. ",[357,1476,1371],{}," is what ",[352,1479,1480],{"href":286},[357,1481,285],{}," wires into a generated project.",[348,1484,1485,1486,1489,1490,1493],{},"Compose ",[357,1487,1488],{},"smtp.Config"," into your project config under a ",[357,1491,1492],{},"MAIL_"," prefix:",[419,1495,1497],{"className":421,"code":1496,"language":423,"meta":424,"style":424},"type Config struct {\n    \u002F\u002F ...\n    Mail smtp.Config `envPrefix:\"MAIL_\"`\n}\n",[357,1498,1499,1511,1516,1537],{"__ignoreMap":424},[428,1500,1501,1503,1506,1509],{"class":430,"line":431},[428,1502,459],{"class":438},[428,1504,1505],{"class":442}," Config",[428,1507,1508],{"class":438}," struct",[428,1510,468],{"class":438},[428,1512,1513],{"class":430,"line":449},[428,1514,1515],{"class":599},"    \u002F\u002F ...\n",[428,1517,1518,1521,1523,1525,1528,1531,1534],{"class":430,"line":456},[428,1519,1520],{"class":553},"    Mail ",[428,1522,1280],{"class":442},[428,1524,488],{"class":438},[428,1526,1527],{"class":442},"Config",[428,1529,1530],{"class":438}," `",[428,1532,1533],{"class":581},"envPrefix:\"MAIL_\"",[428,1535,1536],{"class":438},"`\n",[428,1538,1539],{"class":430,"line":471},[428,1540,516],{"class":438},[655,1542,1543,1556],{},[658,1544,1545],{},[661,1546,1547,1550,1553],{},[664,1548,1549],{},"Variable",[664,1551,1552],{},"Default",[664,1554,1555],{},"Meaning",[671,1557,1558,1574,1589,1605,1634,1660,1677,1688],{},[661,1559,1560,1565,1571],{},[676,1561,1562],{},[357,1563,1564],{},"MAIL_HOST",[676,1566,1567],{},[1568,1569,1570],"em",{},"(empty = no transport)",[676,1572,1573],{},"SMTP server address",[661,1575,1576,1581,1586],{},[676,1577,1578],{},[357,1579,1580],{},"MAIL_PORT",[676,1582,1583],{},[357,1584,1585],{},"587",[676,1587,1588],{},"SMTP port",[661,1590,1591,1599,1602],{},[676,1592,1593,681,1596],{},[357,1594,1595],{},"MAIL_USERNAME",[357,1597,1598],{},"MAIL_PASSWORD",[676,1600,1601],{},"empty",[676,1603,1604],{},"empty: no auth (Mailpit, internal relays)",[661,1606,1607,1612,1617],{},[676,1608,1609],{},[357,1610,1611],{},"MAIL_AUTH",[676,1613,1614],{},[357,1615,1616],{},"auto",[676,1618,1619,1621,1622,1621,1625,1621,1628,1621,1631],{},[357,1620,1616],{}," | ",[357,1623,1624],{},"plain",[357,1626,1627],{},"login",[357,1629,1630],{},"cram-md5",[357,1632,1633],{},"none",[661,1635,1636,1641,1646],{},[676,1637,1638],{},[357,1639,1640],{},"MAIL_TLS",[676,1642,1643],{},[357,1644,1645],{},"starttls",[676,1647,1648,1621,1650,1653,1654,1657,1658],{},[357,1649,1645],{},[357,1651,1652],{},"starttls-opportunistic"," (dev, TLS-less server) | ",[357,1655,1656],{},"tls"," (implicit TLS, 465) | ",[357,1659,1633],{},[661,1661,1662,1666,1674],{},[676,1663,1664],{},[357,1665,690],{},[676,1667,1668],{},[854,1669,1670,1671,1673],{},"required once ",[357,1672,1564],{}," is set",[676,1675,1676],{},"default sender address",[661,1678,1679,1683,1685],{},[676,1680,1681],{},[357,1682,694],{},[676,1684,1601],{},[676,1686,1687],{},"default sender display name",[661,1689,1690,1695,1700],{},[676,1691,1692],{},[357,1693,1694],{},"MAIL_TIMEOUT",[676,1696,1697],{},[357,1698,1699],{},"15s",[676,1701,1702],{},"dial\u002Fsend timeout",[348,1704,1705,1706,488],{},"The full list lives in the ",[352,1707,1708],{"href":53},"configuration reference",[1092,1710,1711,1712,1717,1718,1721,1722,1725,1726,1729,1730,1733,1734,1737,1738,1740,1741,691,1744,1746,1747,1751],{},"A project generated with ",[352,1713,1715],{"href":1714},"\u002Fen\u002Fcli\u002Fnew-project#the-compose-dev-stack",[357,1716,304],{}," already runs a ",[357,1719,1720],{},"mailpit"," service in ",[357,1723,1724],{},"compose.yml"," (accepts SMTP, UI at ",[357,1727,1728],{},"MAILPIT_HOST","), and ",[357,1731,1732],{},".env.example"," ships the matching ",[357,1735,1736],{},"MAIL_*"," lines, but wiring ",[357,1739,1488],{}," into your own project config, and calling ",[357,1742,1743],{},"smtp.NewIfConfigured",[357,1745,1402],{},", happens once, on the first ",[352,1748,1749],{"href":286},[357,1750,285],{}," call.",[1294,1753,1755],{"id":1754},"memory-log-and-discard-for-tests-development-and-no-op-transport","memory, log, and discard: for tests, development, and no-op transport",[419,1757,1759],{"className":421,"code":1758,"language":423,"meta":424,"style":424},"mem := mail.NewMemory()               \u002F\u002F renders and captures, no real sending\nmailer := mail.NewLog(slog.Default()) \u002F\u002F renders and logs instead of sending (dev)\nmailer = mail.NewDiscard()            \u002F\u002F renders (so template bugs still surface) then drops\n",[357,1760,1761,1781,1809],{"__ignoreMap":424},[428,1762,1763,1766,1768,1770,1772,1775,1778],{"class":430,"line":431},[428,1764,1765],{"class":553},"mem ",[428,1767,557],{"class":438},[428,1769,560],{"class":553},[428,1771,488],{"class":438},[428,1773,1774],{"class":474},"NewMemory",[428,1776,1777],{"class":438},"()",[428,1779,1780],{"class":599},"               \u002F\u002F renders and captures, no real sending\n",[428,1782,1783,1785,1787,1789,1791,1794,1796,1799,1801,1803,1806],{"class":430,"line":449},[428,1784,1393],{"class":553},[428,1786,557],{"class":438},[428,1788,560],{"class":553},[428,1790,488],{"class":438},[428,1792,1793],{"class":474},"NewLog",[428,1795,478],{"class":438},[428,1797,1798],{"class":553},"slog",[428,1800,488],{"class":438},[428,1802,1552],{"class":474},[428,1804,1805],{"class":438},"())",[428,1807,1808],{"class":599}," \u002F\u002F renders and logs instead of sending (dev)\n",[428,1810,1811,1813,1816,1818,1820,1823,1825],{"class":430,"line":456},[428,1812,1393],{"class":553},[428,1814,1815],{"class":438},"=",[428,1817,560],{"class":553},[428,1819,488],{"class":438},[428,1821,1822],{"class":474},"NewDiscard",[428,1824,1777],{"class":438},[428,1826,1827],{"class":599},"            \u002F\u002F renders (so template bugs still surface) then drops\n",[348,1829,1830,1833,1834,1837,1838,1841,1842,1845,1846,1849,1850,1477,1853,1855,1856,1858,1859,1861],{},[357,1831,1832],{},"Memory"," renders every message and captures it as ",[357,1835,1836],{},"[]mail.SentMessage"," (",[357,1839,1840],{},"Messages()",", ",[357,1843,1844],{},"Reset()","); it is safe for concurrent use. The ",[357,1847,1848],{},"Log"," driver logs only the subject, recipient count, and rendered size at Info; the full body only at Debug. No PII in logs by default. ",[357,1851,1852],{},"Discard",[357,1854,1743],{}," returns when ",[357,1857,1564],{}," is empty: it still validates and renders (so a broken template fails the same way it would with a real driver), it just never transmits or logs; the quietest possible \"mail is off\" state, distinct from ",[357,1860,1848],{},"'s \"mail is off, but tell me about it\".",[366,1863,1865],{"id":1864},"in-the-shop-the-sendorderconfirmation-listener","In the shop: the sendOrderConfirmation listener",[348,1867,1868,1869,1872,1873,1876,1877,1880,1881,1883,1884,1887],{},"The listener running in the ",[352,1870,1871],{"href":193},"worker"," subscribes to the ",[357,1874,1875],{},"orderPlaced"," ",[352,1878,1879],{"href":178},"event"," and sends via the ",[357,1882,363],{}," it gets from ",[357,1885,1886],{},"Dependencies","; it knows nothing about the driver:",[419,1889,1892],{"className":421,"code":1890,"filename":1891,"language":423,"meta":424,"style":424},"import (\n    \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\"\n    shopevents \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\u002Fevents\"\n)\n\n\u002F\u002Fgo:embed templates\nvar tmpls embed.FS\n\nfunc sendOrderConfirmation(deps shop.Dependencies) func(context.Context, shopevents.OrderPlaced) error {\n    return func(ctx context.Context, ev shopevents.OrderPlaced) error {\n        return deps.Mailer.Send(ctx, mail.NewMessage().\n            WithTo(ev.CustomerEmail).\n            WithSubject(\"Your order confirmation: \"+ev.OrderID).\n            WithBody(mjml.Template(tmpls, \"templates\u002Forder_confirmation.mjml.tmpl\", ev).\n                WithTextTemplate(\"templates\u002Forder_confirmation.txt.tmpl\")))\n    }\n}\n","internal\u002Fmodules\u002Fshop\u002Flisteners\u002Fsend_order_confirmation.go",[357,1893,1894,1901,1911,1923,1927,1931,1935,1947,1951,2000,2032,2063,2080,2106,2137,2153,2159],{"__ignoreMap":424},[428,1895,1896,1898],{"class":430,"line":431},[428,1897,435],{"class":434},[428,1899,1900],{"class":438}," (\n",[428,1902,1903,1906,1909],{"class":430,"line":449},[428,1904,1905],{"class":438},"    \"",[428,1907,1908],{"class":442},"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop",[428,1910,446],{"class":438},[428,1912,1913,1916,1918,1921],{"class":430,"line":456},[428,1914,1915],{"class":553},"    shopevents ",[428,1917,578],{"class":438},[428,1919,1920],{"class":442},"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\u002Fevents",[428,1922,446],{"class":438},[428,1924,1925],{"class":430,"line":471},[428,1926,650],{"class":438},[428,1928,1929],{"class":430,"line":513},[428,1930,453],{"emptyLinePlaceholder":452},[428,1932,1933],{"class":430,"line":942},[428,1934,956],{"class":599},[428,1936,1937,1939,1941,1943,1945],{"class":430,"line":947},[428,1938,962],{"class":438},[428,1940,965],{"class":553},[428,1942,968],{"class":442},[428,1944,488],{"class":438},[428,1946,973],{"class":442},[428,1948,1949],{"class":430,"line":953},[428,1950,453],{"emptyLinePlaceholder":452},[428,1952,1953,1956,1959,1961,1964,1967,1969,1971,1973,1976,1979,1981,1983,1985,1988,1990,1993,1995,1998],{"class":430,"line":959},[428,1954,1955],{"class":438},"func",[428,1957,1958],{"class":474}," sendOrderConfirmation",[428,1960,478],{"class":438},[428,1962,1963],{"class":481},"deps",[428,1965,1966],{"class":442}," shop",[428,1968,488],{"class":438},[428,1970,1886],{"class":442},[428,1972,506],{"class":438},[428,1974,1975],{"class":438}," func(",[428,1977,1978],{"class":442},"context",[428,1980,488],{"class":438},[428,1982,491],{"class":442},[428,1984,494],{"class":438},[428,1986,1987],{"class":442}," shopevents",[428,1989,488],{"class":438},[428,1991,1992],{"class":442},"OrderPlaced",[428,1994,506],{"class":438},[428,1996,1997],{"class":509}," error",[428,1999,468],{"class":438},[428,2001,2002,2005,2007,2009,2011,2013,2015,2017,2020,2022,2024,2026,2028,2030],{"class":430,"line":976},[428,2003,2004],{"class":434},"    return",[428,2006,1975],{"class":438},[428,2008,482],{"class":481},[428,2010,485],{"class":442},[428,2012,488],{"class":438},[428,2014,491],{"class":442},[428,2016,494],{"class":438},[428,2018,2019],{"class":481}," ev",[428,2021,1987],{"class":442},[428,2023,488],{"class":438},[428,2025,1992],{"class":442},[428,2027,506],{"class":438},[428,2029,1997],{"class":509},[428,2031,468],{"class":438},[428,2033,2034,2037,2040,2042,2045,2047,2049,2051,2053,2055,2057,2059,2061],{"class":430,"line":981},[428,2035,2036],{"class":434},"        return",[428,2038,2039],{"class":553}," deps",[428,2041,488],{"class":438},[428,2043,2044],{"class":553},"Mailer",[428,2046,488],{"class":438},[428,2048,521],{"class":474},[428,2050,478],{"class":438},[428,2052,482],{"class":553},[428,2054,494],{"class":438},[428,2056,560],{"class":553},[428,2058,488],{"class":438},[428,2060,565],{"class":474},[428,2062,568],{"class":438},[428,2064,2065,2068,2070,2073,2075,2078],{"class":430,"line":1020},[428,2066,2067],{"class":474},"            WithTo",[428,2069,478],{"class":438},[428,2071,2072],{"class":553},"ev",[428,2074,488],{"class":438},[428,2076,2077],{"class":553},"CustomerEmail",[428,2079,617],{"class":438},[428,2081,2082,2085,2087,2089,2092,2094,2097,2099,2101,2104],{"class":430,"line":1037},[428,2083,2084],{"class":474},"            WithSubject",[428,2086,478],{"class":438},[428,2088,578],{"class":438},[428,2090,2091],{"class":581},"Your order confirmation: ",[428,2093,578],{"class":438},[428,2095,2096],{"class":438},"+",[428,2098,2072],{"class":553},[428,2100,488],{"class":438},[428,2102,2103],{"class":553},"OrderID",[428,2105,617],{"class":438},[428,2107,2108,2111,2113,2115,2117,2119,2121,2123,2125,2127,2129,2131,2133,2135],{"class":430,"line":1042},[428,2109,2110],{"class":474},"            WithBody",[428,2112,478],{"class":438},[428,2114,1104],{"class":553},[428,2116,488],{"class":438},[428,2118,996],{"class":474},[428,2120,478],{"class":438},[428,2122,1001],{"class":553},[428,2124,494],{"class":438},[428,2126,439],{"class":438},[428,2128,1179],{"class":581},[428,2130,578],{"class":438},[428,2132,494],{"class":438},[428,2134,2019],{"class":553},[428,2136,617],{"class":438},[428,2138,2139,2142,2144,2146,2148,2150],{"class":430,"line":1048},[428,2140,2141],{"class":474},"                WithTextTemplate",[428,2143,478],{"class":438},[428,2145,578],{"class":438},[428,2147,1198],{"class":581},[428,2149,578],{"class":438},[428,2151,2152],{"class":438},")))\n",[428,2154,2156],{"class":430,"line":2155},16,[428,2157,2158],{"class":438},"    }\n",[428,2160,2162],{"class":430,"line":2161},17,[428,2163,516],{"class":438},[348,2165,2166,2167,2170,2171,2173,2174,2177],{},"The registration lives in ",[357,2168,2169],{},"listeners\u002Fregister.go",", routed to the ",[357,2172,405],{}," queue (generated by ",[357,2175,2176],{},"add listener shop orderPlaced sendOrderConfirmation --queue mail","):",[419,2179,2182],{"className":421,"code":2180,"filename":2181,"language":423,"meta":424,"style":424},"func Register(reg *events.Registry, deps shop.Dependencies) {\n    events.Listen(reg, \"sendOrderConfirmation\", sendOrderConfirmation(deps), queue.OnQueue(\"mail\"))\n}\n","internal\u002Fmodules\u002Fshop\u002Flisteners\u002Fregister.go",[357,2183,2184,2220,2271],{"__ignoreMap":424},[428,2185,2186,2188,2191,2193,2196,2198,2201,2203,2206,2208,2210,2212,2214,2216,2218],{"class":430,"line":431},[428,2187,1955],{"class":438},[428,2189,2190],{"class":474}," Register",[428,2192,478],{"class":438},[428,2194,2195],{"class":481},"reg",[428,2197,500],{"class":438},[428,2199,2200],{"class":442},"events",[428,2202,488],{"class":438},[428,2204,2205],{"class":442},"Registry",[428,2207,494],{"class":438},[428,2209,2039],{"class":481},[428,2211,1966],{"class":442},[428,2213,488],{"class":438},[428,2215,1886],{"class":442},[428,2217,506],{"class":438},[428,2219,468],{"class":438},[428,2221,2222,2225,2227,2230,2232,2234,2236,2238,2240,2242,2244,2246,2248,2250,2253,2256,2258,2261,2263,2265,2267,2269],{"class":430,"line":449},[428,2223,2224],{"class":553},"    events",[428,2226,488],{"class":438},[428,2228,2229],{"class":474},"Listen",[428,2231,478],{"class":438},[428,2233,2195],{"class":553},[428,2235,494],{"class":438},[428,2237,439],{"class":438},[428,2239,359],{"class":581},[428,2241,578],{"class":438},[428,2243,494],{"class":438},[428,2245,1958],{"class":474},[428,2247,478],{"class":438},[428,2249,1963],{"class":553},[428,2251,2252],{"class":438},"),",[428,2254,2255],{"class":553}," queue",[428,2257,488],{"class":438},[428,2259,2260],{"class":474},"OnQueue",[428,2262,478],{"class":438},[428,2264,578],{"class":438},[428,2266,405],{"class":581},[428,2268,578],{"class":438},[428,2270,939],{"class":438},[428,2272,2273],{"class":430,"line":456},[428,2274,516],{"class":438},[348,2276,2277,2278,2281,2282,2285],{},"Delivery is at-least-once, so the listener should be idempotent: the ",[357,2279,2280],{},"Meta.ID"," read from ",[357,2283,2284],{},"events.MetaFromContext(ctx)"," is a stable idempotency key across retries.",[1294,2287,2289],{"id":2288},"the-test-memory-mailer","The test: Memory mailer",[419,2291,2293],{"className":421,"code":2292,"language":423,"meta":424,"style":424},"func TestSendOrderConfirmation(t *testing.T) {\n    mem := mail.NewMemory()\n    handle := sendOrderConfirmation(shop.Dependencies{Mailer: mem})\n\n    err := handle(t.Context(), shopevents.OrderPlaced{\n        OrderID: \"o-1001\", CustomerEmail: \"user@example.com\",\n    })\n\n    require.NoError(t, err)\n    sent := mem.Messages()\n    require.Len(t, sent, 1)\n    require.Equal(t, \"user@example.com\", sent[0].To[0].Address)\n    require.Contains(t, sent[0].Content.HTML, \"o-1001\") \u002F\u002F Memory really renders\n}\n",[357,2294,2295,2321,2337,2369,2373,2403,2433,2438,2442,2463,2479,2505,2553,2596],{"__ignoreMap":424},[428,2296,2297,2299,2302,2304,2307,2309,2312,2314,2317,2319],{"class":430,"line":431},[428,2298,1955],{"class":438},[428,2300,2301],{"class":474}," TestSendOrderConfirmation",[428,2303,478],{"class":438},[428,2305,2306],{"class":481},"t",[428,2308,500],{"class":438},[428,2310,2311],{"class":442},"testing",[428,2313,488],{"class":438},[428,2315,2316],{"class":442},"T",[428,2318,506],{"class":438},[428,2320,468],{"class":438},[428,2322,2323,2326,2328,2330,2332,2334],{"class":430,"line":449},[428,2324,2325],{"class":553},"    mem ",[428,2327,557],{"class":438},[428,2329,560],{"class":553},[428,2331,488],{"class":438},[428,2333,1774],{"class":474},[428,2335,2336],{"class":438},"()\n",[428,2338,2339,2342,2344,2346,2348,2351,2353,2355,2358,2360,2363,2366],{"class":430,"line":456},[428,2340,2341],{"class":553},"    handle ",[428,2343,557],{"class":438},[428,2345,1958],{"class":474},[428,2347,478],{"class":438},[428,2349,2350],{"class":442},"shop",[428,2352,488],{"class":438},[428,2354,1886],{"class":442},[428,2356,2357],{"class":438},"{",[428,2359,2044],{"class":553},[428,2361,2362],{"class":438},":",[428,2364,2365],{"class":553}," mem",[428,2367,2368],{"class":438},"})\n",[428,2370,2371],{"class":430,"line":471},[428,2372,453],{"emptyLinePlaceholder":452},[428,2374,2375,2378,2380,2383,2385,2387,2389,2391,2394,2396,2398,2400],{"class":430,"line":513},[428,2376,2377],{"class":553},"    err ",[428,2379,557],{"class":438},[428,2381,2382],{"class":474}," handle",[428,2384,478],{"class":438},[428,2386,2306],{"class":553},[428,2388,488],{"class":438},[428,2390,491],{"class":474},[428,2392,2393],{"class":438},"(),",[428,2395,1987],{"class":442},[428,2397,488],{"class":438},[428,2399,1992],{"class":442},[428,2401,2402],{"class":438},"{\n",[428,2404,2405,2408,2410,2412,2415,2417,2419,2422,2424,2426,2428,2430],{"class":430,"line":942},[428,2406,2407],{"class":553},"        OrderID",[428,2409,2362],{"class":438},[428,2411,439],{"class":438},[428,2413,2414],{"class":581},"o-1001",[428,2416,578],{"class":438},[428,2418,494],{"class":438},[428,2420,2421],{"class":553}," CustomerEmail",[428,2423,2362],{"class":438},[428,2425,439],{"class":438},[428,2427,612],{"class":581},[428,2429,578],{"class":438},[428,2431,2432],{"class":438},",\n",[428,2434,2435],{"class":430,"line":947},[428,2436,2437],{"class":438},"    })\n",[428,2439,2440],{"class":430,"line":953},[428,2441,453],{"emptyLinePlaceholder":452},[428,2443,2444,2447,2449,2452,2454,2456,2458,2461],{"class":430,"line":959},[428,2445,2446],{"class":553},"    require",[428,2448,488],{"class":438},[428,2450,2451],{"class":474},"NoError",[428,2453,478],{"class":438},[428,2455,2306],{"class":553},[428,2457,494],{"class":438},[428,2459,2460],{"class":553}," err",[428,2462,650],{"class":438},[428,2464,2465,2468,2470,2472,2474,2477],{"class":430,"line":976},[428,2466,2467],{"class":553},"    sent ",[428,2469,557],{"class":438},[428,2471,2365],{"class":553},[428,2473,488],{"class":438},[428,2475,2476],{"class":474},"Messages",[428,2478,2336],{"class":438},[428,2480,2481,2483,2485,2488,2490,2492,2494,2497,2499,2503],{"class":430,"line":981},[428,2482,2446],{"class":553},[428,2484,488],{"class":438},[428,2486,2487],{"class":474},"Len",[428,2489,478],{"class":438},[428,2491,2306],{"class":553},[428,2493,494],{"class":438},[428,2495,2496],{"class":553}," sent",[428,2498,494],{"class":438},[428,2500,2502],{"class":2501},"sbssI"," 1",[428,2504,650],{"class":438},[428,2506,2507,2509,2511,2514,2516,2518,2520,2522,2524,2526,2528,2530,2533,2536,2539,2542,2544,2546,2548,2551],{"class":430,"line":1020},[428,2508,2446],{"class":553},[428,2510,488],{"class":438},[428,2512,2513],{"class":474},"Equal",[428,2515,478],{"class":438},[428,2517,2306],{"class":553},[428,2519,494],{"class":438},[428,2521,439],{"class":438},[428,2523,612],{"class":581},[428,2525,578],{"class":438},[428,2527,494],{"class":438},[428,2529,2496],{"class":553},[428,2531,2532],{"class":438},"[",[428,2534,2535],{"class":2501},"0",[428,2537,2538],{"class":438},"].",[428,2540,2541],{"class":553},"To",[428,2543,2532],{"class":438},[428,2545,2535],{"class":2501},[428,2547,2538],{"class":438},[428,2549,2550],{"class":553},"Address",[428,2552,650],{"class":438},[428,2554,2555,2557,2559,2562,2564,2566,2568,2570,2572,2574,2576,2579,2581,2583,2585,2587,2589,2591,2593],{"class":430,"line":1037},[428,2556,2446],{"class":553},[428,2558,488],{"class":438},[428,2560,2561],{"class":474},"Contains",[428,2563,478],{"class":438},[428,2565,2306],{"class":553},[428,2567,494],{"class":438},[428,2569,2496],{"class":553},[428,2571,2532],{"class":438},[428,2573,2535],{"class":2501},[428,2575,2538],{"class":438},[428,2577,2578],{"class":553},"Content",[428,2580,488],{"class":438},[428,2582,915],{"class":553},[428,2584,494],{"class":438},[428,2586,439],{"class":438},[428,2588,2414],{"class":581},[428,2590,578],{"class":438},[428,2592,506],{"class":438},[428,2594,2595],{"class":599}," \u002F\u002F Memory really renders\n",[428,2597,2598],{"class":430,"line":1042},[428,2599,516],{"class":438},[348,2601,2602,2603,2605,2606,2608],{},"Because ",[357,2604,1832],{}," actually renders, the test catches template errors too: a misspelled field fails on the ",[357,2607,2578],{}," assertion, not in production.",[366,2610,2612],{"id":2611},"queued-asynchronous-sending","Queued (asynchronous) sending",[348,2614,1124,2615,2617,2618,2621,2622,2624],{},[357,2616,2044],{}," stays synchronous; asynchronous sending follows the pattern above: the event goes to the queue through the ",[352,2619,2620],{"href":183},"outbox",", and the listener sends the email in the worker, with retries. There is no separate built-in \"mail queue\": the ",[352,2623,2200],{"href":178}," system is it.",[366,2626,2628,2629],{"id":2627},"where-the-content-lives-add-mail","Where the content lives: ",[357,2630,285],{},[348,2632,1124,2633,2635,2636,2639],{},[357,2634,359],{}," example above calls ",[357,2637,2638],{},"deps.Mailer.Send"," directly from the listener: fine for a single, simple message. Once a module sends more than one related notification, or the message composition (multiple recipients, a computed subject, several templates) is worth naming and testing on its own, generate a notifier instead:",[419,2641,2645],{"className":2642,"code":2643,"language":2644,"meta":424,"style":424},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","gpsystem add mail \u003Cmodule> \u003Cname>\n","sh",[357,2646,2647],{"__ignoreMap":424},[428,2648,2649,2652,2655,2657,2660,2663,2666,2669,2671,2674,2676],{"class":430,"line":431},[428,2650,2651],{"class":442},"gpsystem",[428,2653,2654],{"class":581}," add",[428,2656,560],{"class":581},[428,2658,2659],{"class":438}," \u003C",[428,2661,2662],{"class":581},"modul",[428,2664,2665],{"class":553},"e",[428,2667,2668],{"class":438},">",[428,2670,2659],{"class":438},[428,2672,2673],{"class":581},"nam",[428,2675,2665],{"class":553},[428,2677,2678],{"class":438},">\n",[348,2680,2681,2682,2685,2686,2690,2691,2695,2696,2698,2699,2702,2703,2706,2707,2709,2710,2713,2714,2716,2717,691,2719,2721,2722,2727],{},"This is the same split the kit uses everywhere: ",[854,2683,2684],{},"the transport is infrastructure, the content is the module's business logic"," (see ",[352,2687,2689],{"href":2688},"\u002Fen\u002Fgetting-started\u002Fproject-structure#what-can-live-inside-a-module","Project structure: what can live inside a module"," and the ",[352,2692,2694],{"href":2693},"\u002Fen\u002Fconcepts\u002Farchitecture#shared-code-a-decision-ladder","shared-code decision ladder","). ",[357,2697,285],{}," generates ",[357,2700,2701],{},"internal\u002Fmodules\u002F\u003Cmodule>\u002Fmail\u002F\u003Cname>.go",": a payload struct, a ",[357,2704,2705],{},"\u003CName>Notifier"," composing an injected ",[357,2708,363],{}," with the module's own templates, and wires a ",[357,2711,2712],{},"Mailer mail.Mailer"," field into the module's ",[357,2715,1886],{},", constructed once, in every entry point, via ",[357,2718,1743],{},[357,2720,1402],{},". See the ",[352,2723,2724,2726],{"href":286},[357,2725,285],{}," reference"," for the exact generated shape.",[1294,2729,2731],{"id":2730},"worked-example-the-contact-email-flow","Worked example: the contact-email flow",[348,2733,2734],{},"A concrete, end-to-end version of this pattern: a web form that notifies both an admin inbox and the requester:",[2736,2737,2738,2751,2760,2769],"ol",{},[385,2739,2740,2743,2744,2747,2748,2750],{},[357,2741,2742],{},"POST \u002Fweb\u002Fcontact",": the web surface's service persists the submission and dispatches a ",[357,2745,2746],{},"ContactSubmitted"," event through the ",[352,2749,2620],{"href":183},", inside the same transaction.",[385,2752,2753,2755,2756,2759],{},[357,2754,1287],{},"'s outbox relay delivers the event to the ",[357,2757,2758],{},"sendContactEmail"," listener.",[385,2761,2762,2763,2766,2767,488],{},"The listener calls ",[357,2764,2765],{},"internal\u002Fmodules\u002Fcontact\u002Fmail.Notifier.Notify",", which renders both templates (an admin notification and a requester confirmation) and sends each through the injected ",[357,2768,363],{},[385,2770,2771,2772,2774,2775,2777,2778,2780,2781,2783],{},"That ",[357,2773,363],{}," is SMTP in production; ",[357,2776,1464],{}," in dev when ",[357,2779,1564],{}," is empty; and ",[357,2782,412],{}," in the notifier's own tests (see above).",[419,2785,2788],{"className":421,"code":2786,"filename":2787,"language":423,"meta":424,"style":424},"package mail\n\n\u002F\u002Fgo:embed templates\u002F*.tmpl\nvar templatesFS embed.FS\n\ntype ContactNotification struct {\n    Name, Email string\n    \u002F\u002F ... the rest of the submitted form\n}\n\ntype Notifier interface {\n    Notify(ctx context.Context, n ContactNotification) error\n}\n\nfunc New(m kitmail.Mailer, cfg Config) Notifier {\n    return &notifier{mailer: m, adminTo: cfg.To}\n}\n\nfunc (n *notifier) Notify(ctx context.Context, p ContactNotification) error {\n    adminErr := n.sendAdminNotification(ctx, p)      \u002F\u002F -> Config.To\n    requesterErr := n.sendRequesterConfirmation(ctx, p) \u002F\u002F -> p.Email\n    return errors.Join(adminErr, requesterErr)\n}\n","internal\u002Fmodules\u002Fcontact\u002Fmail\u002Fnotifier.go",[357,2789,2790,2798,2802,2807,2820,2824,2835,2848,2853,2857,2861,2872,2898,2902,2906,2937,2971,2975,2980,3023,3051,3079,3104],{"__ignoreMap":424},[428,2791,2792,2795],{"class":430,"line":431},[428,2793,2794],{"class":438},"package",[428,2796,2797],{"class":442}," mail\n",[428,2799,2800],{"class":430,"line":449},[428,2801,453],{"emptyLinePlaceholder":452},[428,2803,2804],{"class":430,"line":456},[428,2805,2806],{"class":599},"\u002F\u002Fgo:embed templates\u002F*.tmpl\n",[428,2808,2809,2811,2814,2816,2818],{"class":430,"line":471},[428,2810,962],{"class":438},[428,2812,2813],{"class":553}," templatesFS ",[428,2815,968],{"class":442},[428,2817,488],{"class":438},[428,2819,973],{"class":442},[428,2821,2822],{"class":430,"line":513},[428,2823,453],{"emptyLinePlaceholder":452},[428,2825,2826,2828,2831,2833],{"class":430,"line":942},[428,2827,459],{"class":438},[428,2829,2830],{"class":442}," ContactNotification",[428,2832,1508],{"class":438},[428,2834,468],{"class":438},[428,2836,2837,2840,2842,2845],{"class":430,"line":947},[428,2838,2839],{"class":553},"    Name",[428,2841,494],{"class":438},[428,2843,2844],{"class":553}," Email ",[428,2846,2847],{"class":509},"string\n",[428,2849,2850],{"class":430,"line":953},[428,2851,2852],{"class":599},"    \u002F\u002F ... the rest of the submitted form\n",[428,2854,2855],{"class":430,"line":959},[428,2856,516],{"class":438},[428,2858,2859],{"class":430,"line":976},[428,2860,453],{"emptyLinePlaceholder":452},[428,2862,2863,2865,2868,2870],{"class":430,"line":981},[428,2864,459],{"class":438},[428,2866,2867],{"class":442}," Notifier",[428,2869,465],{"class":438},[428,2871,468],{"class":438},[428,2873,2874,2877,2879,2881,2883,2885,2887,2889,2892,2894,2896],{"class":430,"line":1020},[428,2875,2876],{"class":474},"    Notify",[428,2878,478],{"class":438},[428,2880,482],{"class":481},[428,2882,485],{"class":442},[428,2884,488],{"class":438},[428,2886,491],{"class":442},[428,2888,494],{"class":438},[428,2890,2891],{"class":481}," n",[428,2893,2830],{"class":442},[428,2895,506],{"class":438},[428,2897,510],{"class":509},[428,2899,2900],{"class":430,"line":1037},[428,2901,516],{"class":438},[428,2903,2904],{"class":430,"line":1042},[428,2905,453],{"emptyLinePlaceholder":452},[428,2907,2908,2910,2913,2915,2918,2921,2923,2925,2927,2929,2931,2933,2935],{"class":430,"line":1048},[428,2909,1955],{"class":438},[428,2911,2912],{"class":474}," New",[428,2914,478],{"class":438},[428,2916,2917],{"class":481},"m",[428,2919,2920],{"class":442}," kitmail",[428,2922,488],{"class":438},[428,2924,2044],{"class":442},[428,2926,494],{"class":438},[428,2928,1344],{"class":481},[428,2930,1505],{"class":442},[428,2932,506],{"class":438},[428,2934,2867],{"class":442},[428,2936,468],{"class":438},[428,2938,2939,2941,2944,2947,2949,2951,2953,2956,2958,2961,2963,2965,2967,2969],{"class":430,"line":2155},[428,2940,2004],{"class":434},[428,2942,2943],{"class":438}," &",[428,2945,2946],{"class":442},"notifier",[428,2948,2357],{"class":438},[428,2950,1320],{"class":553},[428,2952,2362],{"class":438},[428,2954,2955],{"class":553}," m",[428,2957,494],{"class":438},[428,2959,2960],{"class":553}," adminTo",[428,2962,2362],{"class":438},[428,2964,1344],{"class":553},[428,2966,488],{"class":438},[428,2968,2541],{"class":553},[428,2970,516],{"class":438},[428,2972,2973],{"class":430,"line":2161},[428,2974,516],{"class":438},[428,2976,2978],{"class":430,"line":2977},18,[428,2979,453],{"emptyLinePlaceholder":452},[428,2981,2983,2985,2987,2990,2993,2995,2997,3000,3002,3004,3006,3008,3010,3012,3015,3017,3019,3021],{"class":430,"line":2982},19,[428,2984,1955],{"class":438},[428,2986,1837],{"class":438},[428,2988,2989],{"class":481},"n ",[428,2991,2992],{"class":438},"*",[428,2994,2946],{"class":442},[428,2996,506],{"class":438},[428,2998,2999],{"class":474}," Notify",[428,3001,478],{"class":438},[428,3003,482],{"class":481},[428,3005,485],{"class":442},[428,3007,488],{"class":438},[428,3009,491],{"class":442},[428,3011,494],{"class":438},[428,3013,3014],{"class":481}," p",[428,3016,2830],{"class":442},[428,3018,506],{"class":438},[428,3020,1997],{"class":509},[428,3022,468],{"class":438},[428,3024,3026,3029,3031,3033,3035,3038,3040,3042,3044,3046,3048],{"class":430,"line":3025},20,[428,3027,3028],{"class":553},"    adminErr ",[428,3030,557],{"class":438},[428,3032,2891],{"class":553},[428,3034,488],{"class":438},[428,3036,3037],{"class":474},"sendAdminNotification",[428,3039,478],{"class":438},[428,3041,482],{"class":553},[428,3043,494],{"class":438},[428,3045,3014],{"class":553},[428,3047,506],{"class":438},[428,3049,3050],{"class":599},"      \u002F\u002F -> Config.To\n",[428,3052,3054,3057,3059,3061,3063,3066,3068,3070,3072,3074,3076],{"class":430,"line":3053},21,[428,3055,3056],{"class":553},"    requesterErr ",[428,3058,557],{"class":438},[428,3060,2891],{"class":553},[428,3062,488],{"class":438},[428,3064,3065],{"class":474},"sendRequesterConfirmation",[428,3067,478],{"class":438},[428,3069,482],{"class":553},[428,3071,494],{"class":438},[428,3073,3014],{"class":553},[428,3075,506],{"class":438},[428,3077,3078],{"class":599}," \u002F\u002F -> p.Email\n",[428,3080,3082,3084,3087,3089,3092,3094,3097,3099,3102],{"class":430,"line":3081},22,[428,3083,2004],{"class":434},[428,3085,3086],{"class":553}," errors",[428,3088,488],{"class":438},[428,3090,3091],{"class":474},"Join",[428,3093,478],{"class":438},[428,3095,3096],{"class":553},"adminErr",[428,3098,494],{"class":438},[428,3100,3101],{"class":553}," requesterErr",[428,3103,650],{"class":438},[428,3105,3107],{"class":430,"line":3106},23,[428,3108,516],{"class":438},[419,3110,3113],{"className":421,"code":3111,"filename":3112,"language":423,"meta":424,"style":424},"func sendContactEmail(deps contact.Dependencies) func(context.Context, contactevents.ContactSubmitted) error {\n    notifier := contactmail.New(deps.Mailer, deps.Mail)\n    return func(ctx context.Context, ev contactevents.ContactSubmitted) error {\n        return notifier.Notify(ctx, contactmail.ContactNotification{Name: ev.Name, Email: ev.Email \u002F* ... *\u002F})\n    }\n}\n","internal\u002Fmodules\u002Fcontact\u002Flisteners\u002Fsendcontactemail.go",[357,3114,3115,3158,3190,3220,3277,3281],{"__ignoreMap":424},[428,3116,3117,3119,3122,3124,3126,3129,3131,3133,3135,3137,3139,3141,3143,3145,3148,3150,3152,3154,3156],{"class":430,"line":431},[428,3118,1955],{"class":438},[428,3120,3121],{"class":474}," sendContactEmail",[428,3123,478],{"class":438},[428,3125,1963],{"class":481},[428,3127,3128],{"class":442}," contact",[428,3130,488],{"class":438},[428,3132,1886],{"class":442},[428,3134,506],{"class":438},[428,3136,1975],{"class":438},[428,3138,1978],{"class":442},[428,3140,488],{"class":438},[428,3142,491],{"class":442},[428,3144,494],{"class":438},[428,3146,3147],{"class":442}," contactevents",[428,3149,488],{"class":438},[428,3151,2746],{"class":442},[428,3153,506],{"class":438},[428,3155,1997],{"class":509},[428,3157,468],{"class":438},[428,3159,3160,3163,3165,3168,3170,3172,3174,3176,3178,3180,3182,3184,3186,3188],{"class":430,"line":449},[428,3161,3162],{"class":553},"    notifier ",[428,3164,557],{"class":438},[428,3166,3167],{"class":553}," contactmail",[428,3169,488],{"class":438},[428,3171,1335],{"class":474},[428,3173,478],{"class":438},[428,3175,1963],{"class":553},[428,3177,488],{"class":438},[428,3179,2044],{"class":553},[428,3181,494],{"class":438},[428,3183,2039],{"class":553},[428,3185,488],{"class":438},[428,3187,1349],{"class":553},[428,3189,650],{"class":438},[428,3191,3192,3194,3196,3198,3200,3202,3204,3206,3208,3210,3212,3214,3216,3218],{"class":430,"line":456},[428,3193,2004],{"class":434},[428,3195,1975],{"class":438},[428,3197,482],{"class":481},[428,3199,485],{"class":442},[428,3201,488],{"class":438},[428,3203,491],{"class":442},[428,3205,494],{"class":438},[428,3207,2019],{"class":481},[428,3209,3147],{"class":442},[428,3211,488],{"class":438},[428,3213,2746],{"class":442},[428,3215,506],{"class":438},[428,3217,1997],{"class":509},[428,3219,468],{"class":438},[428,3221,3222,3224,3227,3229,3232,3234,3236,3238,3240,3242,3245,3247,3250,3252,3254,3256,3258,3260,3263,3265,3267,3269,3272,3275],{"class":430,"line":471},[428,3223,2036],{"class":434},[428,3225,3226],{"class":553}," notifier",[428,3228,488],{"class":438},[428,3230,3231],{"class":474},"Notify",[428,3233,478],{"class":438},[428,3235,482],{"class":553},[428,3237,494],{"class":438},[428,3239,3167],{"class":442},[428,3241,488],{"class":438},[428,3243,3244],{"class":442},"ContactNotification",[428,3246,2357],{"class":438},[428,3248,3249],{"class":553},"Name",[428,3251,2362],{"class":438},[428,3253,2019],{"class":553},[428,3255,488],{"class":438},[428,3257,3249],{"class":553},[428,3259,494],{"class":438},[428,3261,3262],{"class":553}," Email",[428,3264,2362],{"class":438},[428,3266,2019],{"class":553},[428,3268,488],{"class":438},[428,3270,3271],{"class":553},"Email ",[428,3273,3274],{"class":599},"\u002F* ... *\u002F",[428,3276,2368],{"class":438},[428,3278,3279],{"class":430,"line":513},[428,3280,2158],{"class":438},[428,3282,3283],{"class":430,"line":942},[428,3284,516],{"class":438},[348,3286,3287,3288,3291,3292,3294,3295,3297,3298,1444,3301,3303],{},"Both the recipient config (",[357,3289,3290],{},"Config.To",", module content) and the shared SMTP config (",[357,3293,1488],{},", kit-level transport) compose under the same ",[357,3296,1492],{}," prefix in platform config: ",[357,3299,3300],{},"MAIL_TO",[357,3302,1564],{}," are just two fields under one env namespace, with no naming collision:",[419,3305,3308],{"className":421,"code":3306,"filename":3307,"language":423,"meta":424,"style":424},"type Config struct {\n    \u002F\u002F ...\n    Mail        smtp.Config        `envPrefix:\"MAIL_\"` \u002F\u002F MAIL_HOST, MAIL_FROM_ADDRESS, ...\n    ContactMail contactmail.Config `envPrefix:\"MAIL_\"` \u002F\u002F MAIL_TO\n}\n","internal\u002Fplatform\u002Fconfig\u002Fconfig.go",[357,3309,3310,3320,3324,3345,3366],{"__ignoreMap":424},[428,3311,3312,3314,3316,3318],{"class":430,"line":431},[428,3313,459],{"class":438},[428,3315,1505],{"class":442},[428,3317,1508],{"class":438},[428,3319,468],{"class":438},[428,3321,3322],{"class":430,"line":449},[428,3323,1515],{"class":599},[428,3325,3326,3329,3331,3333,3335,3338,3340,3342],{"class":430,"line":456},[428,3327,3328],{"class":553},"    Mail        ",[428,3330,1280],{"class":442},[428,3332,488],{"class":438},[428,3334,1527],{"class":442},[428,3336,3337],{"class":438},"        `",[428,3339,1533],{"class":581},[428,3341,1229],{"class":438},[428,3343,3344],{"class":599}," \u002F\u002F MAIL_HOST, MAIL_FROM_ADDRESS, ...\n",[428,3346,3347,3350,3353,3355,3357,3359,3361,3363],{"class":430,"line":471},[428,3348,3349],{"class":553},"    ContactMail ",[428,3351,3352],{"class":442},"contactmail",[428,3354,488],{"class":438},[428,3356,1527],{"class":442},[428,3358,1530],{"class":438},[428,3360,1533],{"class":581},[428,3362,1229],{"class":438},[428,3364,3365],{"class":599}," \u002F\u002F MAIL_TO\n",[428,3367,3368],{"class":430,"line":513},[428,3369,516],{"class":438},[1092,3371,3372,3374,3375,3377,3378,3380,3381,3384],{},[357,3373,285],{}," does not scaffold the wiring shown in ",[357,3376,2758],{}," above by hand: the generator produces the notifier, its templates and the ",[357,3379,1886],{},"\u002Fconfig wiring; the payload fields, the subject line, and the call from a service or listener are business logic you write, the same way ",[357,3382,3383],{},"service\u002Fservice.go"," ships as a minimal seam.",[348,3386,3387,3388,1444,3390,3394,3395,3398,3399,691,3402,3404],{},"When a module's content is addressed to a recipient rather than a bare address, especially once it needs more than mail (a persisted inbox, a live push), see ",[352,3389,257],{"href":258},[352,3391,3392],{"href":291},[357,3393,290],{}," instead; the two generators compose, and a notification's ",[357,3396,3397],{},"ToMail"," reuses the same ",[357,3400,3401],{},"mail.Message",[357,3403,1085],{}," builders shown here.",[3406,3407,3408],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":424,"searchDepth":449,"depth":449,"links":3410},[3411,3412,3413,3414,3415,3416,3420,3423,3424],{"id":368,"depth":449,"text":369},{"id":416,"depth":449,"text":417},{"id":536,"depth":449,"text":537},{"id":840,"depth":449,"text":841},{"id":1104,"depth":449,"text":1105},{"id":1291,"depth":449,"text":1292,"children":3417},[3418,3419],{"id":1280,"depth":456,"text":1280},{"id":1754,"depth":456,"text":1755},{"id":1864,"depth":449,"text":1865,"children":3421},[3422],{"id":2288,"depth":456,"text":2289},{"id":2611,"depth":449,"text":2612},{"id":2627,"depth":449,"text":3425,"children":3426},"Where the content lives: add mail",[3427],{"id":2730,"depth":456,"text":2731},"Email sending: fluent Message builder, MJML templates, a memory mailer for tests.","md",null,{},{"icon":255},{"title":252,"description":3428},"HFl5hQPzek5jiRKTbP8BwFtekY3lnVoHC8zDYrHNMTQ",[3436,3438],{"title":248,"path":249,"stem":250,"description":3437,"icon":243,"children":-1},"Object storage through the storage.Store interface, with an S3-compatible implementation, without importing the AWS SDK.",{"title":257,"path":258,"stem":259,"description":3439,"icon":260,"children":-1},"Multi-channel notifications: one payload, several channels (database, mail, broadcast), driven by a shared Hub.",1784668710451]