Minden SaaS küld emailt (jelszó-visszaállítás, számla, értesítés), és a legtöbb fogad is (support-cím, válaszok). Ez a modul végigveszi a Cloudflare Email Service két felét, a kézbesíthetőség buktatóit, és hogy multitenant rendszerben mire kell figyelni.
A Cloudflare Email Service két képességet fog össze:
| Email Sending | Email Routing | |
|---|---|---|
| Mit csinál | kimenő tranzakciós email küldése (Workerből, REST API-ból vagy SMTP-n) | bejövő email fogadása: átirányítás címre, vagy Workerbe |
| Tipikus használat | üdvözlő levél, jelszó-visszaállítás, magic link, számla, riasztás | support@, orders@ címek; email mint API |
| Elérhetőség | Workers Paid csomagon (bétában indult) | ingyenes és fizetős csomagon is |
| AWS-megfelelő | SES (küldés) | SES bejövő + Lambda |
{
"send_email": [
{ "name": "EMAIL", "remote": true } // remote: valódi API a `wrangler dev` alatt is
]
}
server/utils/mail.ts
export async function sendMail(env, opts: {
to: string; subject: string; html: string; text: string; replyTo?: string;
}) {
const res = await env.EMAIL.send({
from: "noreply@myapp.com",
to: opts.to,
subject: opts.subject,
html: opts.html,
text: opts.text, // MINDIG legyen sima szöveges változat is
replyTo: opts.replyTo,
});
return res.messageId; // naplózd — ez a nyomkövetési azonosító
}
// 1) az API-végpont csak sorba tesz
await env.JOBS.send({ type: "mail", template: "password-reset",
to: user.email, tenantId, data: { token } });
// 2) a consumer küld — itt van retry, DLQ, és itt mérünk (9. modul)
async queue(batch, env) {
for (const m of batch.messages) {
try {
const { html, text, subject } = renderTemplate(m.body);
const id = await sendMail(env, { to: m.body.to, subject, html, text });
env.USAGE.writeDataPoint({ blobs: [m.body.tenantId, "mail_sent", m.body.template],
doubles: [1], indexes: [m.body.tenantId] });
m.ack();
} catch (e) { m.retry({ delaySeconds: 120 }); }
}
}
A bejövő oldal az, ami a Workers-modellben tényleg más: a levél egy handlerben landol (ugyanabban a Workerben, ahol a fetch és a queue van — 2. modul):
export default {
async email(message, env, ctx) {
const from = message.from; // feladó
const to = message.to; // pl. support@myapp.com vagy acme@tickets.myapp.com
// a nyers üzenet feldolgozása (fejlécek, törzs, mellékletek)
const raw = await new Response(message.raw).text();
// 1) melyik tenanthoz tartozik? — a címzettből vagy a feladóból
const tenantId = tenantFromAddress(to);
// 2) ne itt dolgozzuk fel: sorba tesszük (ugyanaz az elv, mint a webhooknál)
await env.JOBS.send({ type: "inbound-mail", tenantId, from, to, raw });
// 3) opcionálisan továbbítás egy valódi postafiókba is
// await message.forward("support@mycompany.com");
},
};
Mit lehet ezzel csinálni? Ticket nyitása emailből, válaszok hozzáfűzése a meglévő ticketthez (a In-Reply-To fejléc alapján), mellékletek mentése R2-be (6. modul), automatikus feldolgozás (rendelés-visszaigazolás beolvasása), vagy egyszerűen egy okos átirányítás. A levél elutasítható is (message.setReject()), ha nem felel meg a szabályaidnak.
Ez a rész független attól, melyik szolgáltatót használod, és a fejlesztők jellemzően alábecsülik. Három DNS-rekord adja az alapot:
| Rekord | Mit mond | Ha hiányzik |
|---|---|---|
| SPF (TXT) | mely szerverek küldhetnek a domainedről | a leveleid nagy eséllyel spam-be esnek |
| DKIM (TXT) | kriptográfiai aláírás: a levél tényleg tőled jött, és nem módosult | ugyanaz — sok fogadó elutasít |
| DMARC (TXT) | mit tegyen a fogadó, ha az SPF/DKIM nem stimmel (none / quarantine / reject), és hova küldjön jelentést | nincs visszajelzésed, és a nagy szolgáltatók egyre szigorúbban követelik |
Praktikus sorrend a bevezetéshez: állítsd be az SPF-et és a DKIM-et, majd a DMARC-ot először p=none értékkel — így kapsz jelentéseket, de még nem utasít el semmit. Nézd meg pár hétig, mi küld a domainedről (gyakran kiderül, hogy a CRM, a számlázó és a hírlevél-eszköz is), és amikor minden legitim küldő aláírt, lépj quarantine, majd reject felé.
mail.myapp.com vs. news.myapp.com). Ha a hírlevél panaszokat gyűjt, ne az vigye magával a jelszó-visszaállító leveleidet. ② Mindig küldj sima szöveges változatot is a HTML mellé, kerüld a képekből álló leveleket és a link-rövidítőket — ezek klasszikus spam-jelek.| Kérdés | Megoldás |
|---|---|
| Kinek a nevében küldünk? | A legegyszerűbb és legbiztonságosabb: mindig a te domainedről (noreply@myapp.com), a tenant nevével a megjelenítendő névben („Acme Kft. — MyApp"), és a tenant címével Reply-To-ban. Így a te reputációdon fut, amit te kontrollálsz. |
| Az ügyfél saját domainjéről akar küldeni | Ez a „white-label email": ilyenkor az ügyfélnek kell SPF/DKIM-rekordokat felvennie a saját DNS-ébe, és neked ezt az onboarding-folyamatba kell építened (ellenőrzéssel!). Csak akkor vállald, ha tényleg üzleti igény — a hibás beállítás az ő domainjének reputációját rontja. |
| Bejövő cím tenantonként | Aldomain-minta: acme@tickets.myapp.com vagy ticket+acme-123@myapp.com. A címből azonosítod a tenantot — de ellenőrizd a feladót is, mert a feladó-cím hamisítható (ezért ne adj jogosultságot pusztán a From alapján!). |
| Kvóta és visszaélés | Tenantonkénti napi email-limit (DO-számláló, 7. modul), különben egy elszabadult integráció vagy egy visszaélő ügyfél viszi a reputációdat. |
| Mérés | Küldésenként adatpont az Analytics Engine-be (9. modul): tenant, sablon, státusz — ebből lesz a „mennyi emailt küld ez az ügyfél" riport és a számlázási alap. |
send_email binding remote: true beállítással a wrangler dev alatt is a valódi szolgáltatást hívja (11. modul) — ezért dev-ben irányíts mindent egy teszt-postafiókba, vagy tegyél egy kapcsolót a kódba, ami nem-produkciós környezetben csak logol. Egy véletlen éles kiküldés a fejlesztői gépről kellemetlen.wrangler tail (9. modul) mutatja, mi érkezett.messageId-vel), és riasztás, ha a DLQ-ba email-üzenetek kerülnek.Az Email Service kényelmes (binding, nincs külön kulcs, ugyanaz a számla), de nem mindig a helyes választás. Maradj/válts külső szolgáltatóra (Resend, Postmark, SES…), ha:
A jó hír: bármelyiket választod, a szerkezet ugyanaz marad — API-végpont → Queue → küldő consumer. A szolgáltató cseréje így egyetlen függvény cseréje (sendMail), nem architektúra-váltás.
Mert lassú és külső szolgáltatásfüggő: a felhasználó vár, és hiba esetén nincs újrapróbálás. Helyette: az API-végpont sorba tesz, a queue consumer küld — retry-jal, DLQ-val és riasztással, így a hiba látható és javítható marad.
email() handler, amit a klasszikus szerveren nehéz volt megoldani?
A levél közvetlenül a kódodban landol, ugyanabban a Workerben, ahol a HTTP- és queue-handlerek — nincs külön IMAP-poller vagy levelezőszerver. Így az email egy bemeneti csatorna lesz: ticket-nyitás, válasz-hozzáfűzés, melléklet R2-be, automatikus feldolgozás.
SPF, DKIM, DMARC. Sorrend: SPF és DKIM beállítása, majd DMARC p=none-nal (jelentések gyűjtése), és csak azután szigorítás quarantine → reject felé, amikor minden legitim küldő aláírtan küld.
Alapesetben a saját domainedről, a tenant nevével a megjelenített névben és a tenant címével Reply-To-ban — így a te (kontrollált) reputációdon fut. Az ügyfél saját domainjéről való küldés (white-label) csak akkor, ha valódi üzleti igény, és akkor is ellenőrzött SPF/DKIM-beállítással az onboardingban.
remote: true email-bindingnak fejlesztés közben?
Hogy a wrangler dev a valódi szolgáltatást hívja, tehát egy teszt könnyen kiküld éles emailt. Védekezés: nem-produkciós környezetben minden címzettet egy teszt-postafiókba irányíts, vagy csak logolj a tényleges küldés helyett.