{"_id":"@devstevenjs/esendy","_rev":"5-f47531f80c61d578b8fb3b5e951164f4","name":"@devstevenjs/esendy","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@devstevenjs/esendy","version":"1.0.0","keywords":["email","nodemailer","react-email","secure","mailer"],"license":"MIT","_id":"@devstevenjs/esendy@1.0.0","maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"dist":{"shasum":"961ac0977b257b2251ad64dd6ed80236bf0dbd72","tarball":"https://registry.npmjs.org/@devstevenjs/esendy/-/esendy-1.0.0.tgz","fileCount":15,"integrity":"sha512-gw3h1I0GO0GLTtjVpFwrYBZ+6AXoD9ZN1RUke3qhnYLgZ9MSFjUTRusUu7qGAExfvKtJeYFKZRRlMgf6RdOMdA==","signatures":[{"sig":"MEUCIGb5gbQMvPPWlQYsBV18NndtBzt25DxnisDMH2R6DUr6AiEAltxYysdqZqjkGkwFglHIcy8oBz3PWrpAq45EC6hnx4Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115470},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.mjs","require":"./dist/templates/index.js"}},"gitHead":"f785eee05e536e858dff541ea4ff0b1a47d8142d","scripts":{"dev":"tsup --watch","build":"tsup","prepare":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"devstevenjs","email":"dev@devsteven.com"},"_npmVersion":"11.9.0","description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","directories":{},"_nodeVersion":"25.6.1","dependencies":{"validator":"^13.12.0","nodemailer":"^8.0.1","sanitize-html":"^2.13.0","@react-email/render":">=1.0.0","@react-email/components":">=0.0.22"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.0.0","typescript":"^5.0.0","@types/node":"^25.0.0","@types/react":"^19.0.0","@types/validator":"^13.12.0","@types/nodemailer":"^7.0.0","@types/sanitize-html":"^2.13.0"},"peerDependencies":{"react":">=18.0.0 || >=19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/esendy_1.0.0_1772192532361_0.9852714601032855","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@devstevenjs/esendy","version":"1.0.1","keywords":["email","nodemailer","react-email","secure","mailer"],"license":"MIT","_id":"@devstevenjs/esendy@1.0.1","maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"dist":{"shasum":"4c04a5fcf62444cbc74813c64ce2b959a8f2f5ae","tarball":"https://registry.npmjs.org/@devstevenjs/esendy/-/esendy-1.0.1.tgz","fileCount":15,"integrity":"sha512-YLRfRZJFXVcl0ohTCF/jGA/rDfo9jsidU++2j1npQgWeeJjZIGtXZ5U3yuYMPYZnW8uyHhXZkRracdj32wpimQ==","signatures":[{"sig":"MEQCIFLW8mxtufV9TQXiVybSJKWsJtOdy1rew8L/MWxRKIh3AiBaXvyRJfjRX2cwKzRZQyP9c7Rqlk5jKsvmzVDWjyYXyg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115756},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.mjs","require":"./dist/templates/index.js"}},"gitHead":"44be64fc46f70e8f00081848d749febc38fe0d31","scripts":{"dev":"tsup --watch","build":"tsup","prepare":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"devstevenjs","email":"dev@devsteven.com"},"_npmVersion":"11.9.0","description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","directories":{},"_nodeVersion":"25.6.1","dependencies":{"validator":"^13.12.0","nodemailer":"^8.0.1","sanitize-html":"^2.13.0","@react-email/render":">=1.0.0","@react-email/components":">=0.0.22"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.0.0","typescript":"^5.0.0","@types/node":"^25.0.0","@types/react":"^19.0.0","@types/validator":"^13.12.0","@types/nodemailer":"^7.0.0","@types/sanitize-html":"^2.13.0"},"peerDependencies":{"react":">=18.0.0 || >=19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/esendy_1.0.1_1772578346606_0.3241883185001786","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@devstevenjs/esendy","version":"1.0.2","keywords":["email","nodemailer","react-email","secure","mailer"],"license":"MIT","_id":"@devstevenjs/esendy@1.0.2","maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"dist":{"shasum":"53ca00f1697d8cd29c38fd3ebc3b9138f5868964","tarball":"https://registry.npmjs.org/@devstevenjs/esendy/-/esendy-1.0.2.tgz","fileCount":15,"integrity":"sha512-cabUkj2lUbIivR6kWCQVSiVqh1egDHvsm2U8ymGGRFT73rrR65u+0Im29iQraJR99Js5cyPhoFzDqw3D3lBisw==","signatures":[{"sig":"MEUCIQD1bnhSFnlZxDM5QcfpC4Jn6Z/Vh7uWL/s9gOAaVH+jXQIgVKiLTnq1lztor7JeP4/Sb4xjrWNdsWEWkfBaVTZdOOA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":117892},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.mjs","require":"./dist/templates/index.js"}},"gitHead":"d7ef4595f047e9d810ba88f545284d6cf75b21f6","scripts":{"dev":"tsup --watch","build":"tsup","prepare":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"devstevenjs","email":"dev@devsteven.com"},"_npmVersion":"11.9.0","description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","directories":{},"_nodeVersion":"25.6.1","dependencies":{"validator":"^13.12.0","nodemailer":"^8.0.1","sanitize-html":"^2.13.0","@react-email/render":">=1.0.0","@react-email/components":">=0.0.22"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.0.0","typescript":"^5.0.0","@types/node":"^25.0.0","@types/react":"^19.0.0","@types/validator":"^13.12.0","@types/nodemailer":"^7.0.0","@types/sanitize-html":"^2.13.0"},"peerDependencies":{"react":">=18.0.0 || >=19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/esendy_1.0.2_1772625585918_0.4336668045106644","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@devstevenjs/esendy","version":"1.1.0","keywords":["email","nodemailer","react-email","secure","mailer"],"license":"MIT","_id":"@devstevenjs/esendy@1.1.0","maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"dist":{"shasum":"d74f8cfe685b7725333e75d85f6f5b2d7137d156","tarball":"https://registry.npmjs.org/@devstevenjs/esendy/-/esendy-1.1.0.tgz","fileCount":15,"integrity":"sha512-i5qHADH4MIuvIDw7tEKGhx7tfmwBaP1begtdHNtu8TbgI1YcGmpHZimDqfzxgNClHROb/zgSh3tKZxth7ByNpg==","signatures":[{"sig":"MEUCIH0uC7VKHbTb/ye0SVVjUpIZVfIQeEyP7u9kGHCvIMniAiEAks/ymrijGpZQUcIpDPQygps3IEq7p0VvHeiWljn0pT4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":151370},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.mjs","require":"./dist/templates/index.js"}},"gitHead":"ce5163020aa47d4431196882652fd69512d1e711","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","prepare":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"devstevenjs","email":"dev@devsteven.com"},"_npmVersion":"11.16.0","description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","directories":{},"_nodeVersion":"25.6.1","allowScripts":{"esbuild@0.27.3":true,"fsevents@2.3.3":true},"dependencies":{"validator":"^13.12.0","nodemailer":"^8.0.8","sanitize-html":"^2.13.0","@react-email/render":">=1.0.0","@react-email/components":">=0.0.22"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.0.0","vitest":"^4.1.7","typescript":"^5.0.0","@types/node":"^25.0.0","@types/react":"^19.0.0","@types/validator":"^13.12.0","@types/nodemailer":"^7.0.0","@types/sanitize-html":"^2.13.0"},"peerDependencies":{"react":">=18.0.0 || >=19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/esendy_1.1.0_1780326350859_0.9202995733975601","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@devstevenjs/esendy","version":"1.1.1","description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.mjs","require":"./dist/templates/index.js"}},"scripts":{"build":"tsup","prepare":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"react":">=18.0.0 || >=19.0.0"},"dependencies":{"@react-email/components":">=0.0.22","@react-email/render":">=1.0.0","nodemailer":"^8.0.8","sanitize-html":"^2.13.0","validator":"^13.12.0"},"devDependencies":{"@types/node":"^25.0.0","@types/nodemailer":"^7.0.0","@types/react":"^19.0.0","@types/sanitize-html":"^2.13.0","@types/validator":"^13.12.0","react":"^19.0.0","tsup":"^8.0.0","typescript":"^5.0.0","vitest":"^4.1.7"},"license":"MIT","publishConfig":{"access":"public"},"keywords":["email","nodemailer","react-email","secure","mailer"],"allowScripts":{"esbuild@0.27.3":true,"fsevents@2.3.3":true},"gitHead":"c2c9dc541949b38c959ebdfd935ad4adaee9a00c","_id":"@devstevenjs/esendy@1.1.1","_nodeVersion":"25.6.1","_npmVersion":"11.16.0","dist":{"integrity":"sha512-DlUbnr+LZqn9RcFYiEWdhHZanEC50k2YyyN/2hBIhFPy1cgOViVtddnzBfcOSB1xbODUP0wQsUMdypQd8c5vLA==","shasum":"340e9f3833acfb5884226d51c3ab1f923b7c8e62","tarball":"https://registry.npmjs.org/@devstevenjs/esendy/-/esendy-1.1.1.tgz","fileCount":15,"unpackedSize":156564,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD5CWF6KtaOKuVg78yv/EWaxoFN92wFsIntubsv2ywXSgIhANQ0+A59/fi4wYHPYwLK+LOH1r4NXWgQWn90/3b4UtK4"}]},"_npmUser":{"name":"devstevenjs","email":"dev@devsteven.com"},"directories":{},"maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/esendy_1.1.1_1780328903318_0.10239653836136076"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-27T11:42:12.245Z","modified":"2026-06-01T15:48:23.597Z","1.0.0":"2026-02-27T11:42:12.526Z","1.0.1":"2026-03-03T22:52:26.766Z","1.0.2":"2026-03-04T11:59:46.049Z","1.1.0":"2026-06-01T15:05:51.005Z","1.1.1":"2026-06-01T15:48:23.473Z"},"license":"MIT","keywords":["email","nodemailer","react-email","secure","mailer"],"description":"Secure, reusable email sender with React Email templates for Next.js and Vite projects","maintainers":[{"name":"devstevenjs","email":"dev@devsteven.com"}],"readme":"# esendy\n\nSecure, reusable email sender for Next.js and Vite projects. Built on top of nodemailer with React Email templates, input sanitization, header injection prevention, and built-in rate limiting.\n\n---\n\n## Features\n\n- SMTP with enforced TLS (no plaintext fallback, cert verification always on)\n- CRLF header injection prevention on all header fields\n- HTML sanitization via `sanitize-html` (strips XSS, javascript: URLs, dangerous attributes)\n- RFC 5322 email address validation\n- Sliding window rate limiter (in-memory, per key)\n- React Email templates with opinionated default design, fully overridable per project\n- Auto-generated plain-text fallback from HTML\n- Optional Telegram delivery — mirrors each sent email to a single configured group via a bot (native `fetch`, zero new dependencies)\n- TypeScript-first, full type definitions\n\n---\n\n## Installation\n\n```bash\nnpm install @devstevenjs/esendy\n```\n\nFor projects on React 18, add `--legacy-peer-deps` (esendy's React Email renderer requires React 19). Next.js 15+ projects are on React 19 by default and install cleanly.\n\n---\n\n## Environment Variables\n\nEsendy never reads env vars directly — you pass the config explicitly so it works in any framework. These are the typical env vars you define in each project:\n\n```env\nESENDY_SMTP_HOST=mail.yourdomain.com\nESENDY_SMTP_PORT=587\nESENDY_SMTP_USER=no-reply@yourdomain.com\nESENDY_SMTP_PASS=yourpassword\nESENDY_FROM_NAME=My App\nESENDY_TO=owner@yourdomain.com\n\n# Optional — Telegram delivery (see \"Telegram delivery\" section below)\nESENDY_TELEGRAM_BOT_TOKEN=123456:ABC-DEF...\nESENDY_TELEGRAM_CHAT_ID=-1001234567890\n```\n\n---\n\n## Quick Start\n\n### 1. Create a shared mailer instance\n\nCreate this file once per project and import it wherever you need to send email.\n\n```ts\n// lib/mailer.ts  (Next.js)\n// src/lib/mailer.ts  (Vite + Express/Fastify)\n\nimport { createEsendy } from '@devstevenjs/esendy'\n\nexport const mailer = createEsendy({\n  smtp: {\n    host: process.env.ESENDY_SMTP_HOST!,\n    port: Number(process.env.ESENDY_SMTP_PORT) || 587,\n    auth: {\n      user: process.env.ESENDY_SMTP_USER!,\n      pass: process.env.ESENDY_SMTP_PASS!,\n    },\n  },\n  from: {\n    name: process.env.ESENDY_FROM_NAME ?? 'My App',\n    address: process.env.ESENDY_SMTP_USER!,\n  },\n  rateLimit: {\n    windowMs: 60_000, // 1 minute\n    max: 5,           // max 5 emails per minute per key (typically per IP)\n  },\n  defaults: {\n    siteName: 'My App',\n    primaryColor: '#2563eb',  // override per project\n    // logoUrl: 'https://yourdomain.com/logo.png',\n    // footerText: '© 2026 My App · yourdomain.com',\n  },\n  // Optional — mirrors every sent email to a Telegram group (gracefully no-ops when unset)\n  telegram: process.env.ESENDY_TELEGRAM_BOT_TOKEN\n    ? {\n        botToken: process.env.ESENDY_TELEGRAM_BOT_TOKEN,\n        chatId: process.env.ESENDY_TELEGRAM_CHAT_ID!,\n      }\n    : undefined,\n})\n```\n\n---\n\n## Usage: Contact Form\n\n### Next.js (App Router — API Route)\n\n```ts\n// app/api/contact/route.ts\nimport { NextRequest, NextResponse } from 'next/server'\nimport { mailer } from '@/lib/mailer'\nimport { ContactTemplate } from '@devstevenjs/esendy/templates'\nimport { EsendyRateLimitError, EsendyValidationError } from '@devstevenjs/esendy'\n\nexport async function POST(req: NextRequest) {\n  const ip = req.headers.get('x-forwarded-for') ?? req.headers.get('x-real-ip') ?? 'unknown'\n\n  try {\n    const body = await req.json()\n    const { name, email, message } = body\n\n    // Basic presence check before hitting mailer validation\n    if (!name || !email || !message) {\n      return NextResponse.json({ error: 'All fields are required' }, { status: 400 })\n    }\n\n    await mailer.send({\n      to: process.env.ESENDY_TO!,\n      subject: `New contact from ${name}`,\n      template: (\n        <ContactTemplate\n          senderName={name}\n          senderEmail={email}\n          message={message}\n          // Template design — override project defaults here if needed\n          siteName=\"My App\"\n          primaryColor=\"#2563eb\"\n          // extraFields={{ Phone: body.phone }}  // optional extra fields\n        />\n      ),\n      rateLimitKey: ip,\n    })\n\n    return NextResponse.json({ success: true })\n  } catch (err) {\n    if (err instanceof EsendyRateLimitError) {\n      return NextResponse.json(\n        { error: 'Too many requests. Please wait and try again.' },\n        { status: 429 }\n      )\n    }\n    if (err instanceof EsendyValidationError) {\n      return NextResponse.json({ error: err.message }, { status: 400 })\n    }\n    console.error('[esendy] send failed:', err)\n    return NextResponse.json({ error: 'Failed to send message' }, { status: 500 })\n  }\n}\n```\n\n### Next.js (Pages Router — API Route)\n\n```ts\n// pages/api/contact.ts\nimport type { NextApiRequest, NextApiResponse } from 'next'\nimport { mailer } from '@/lib/mailer'\nimport { ContactTemplate } from '@devstevenjs/esendy/templates'\nimport { EsendyRateLimitError, EsendyValidationError } from '@devstevenjs/esendy'\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  if (req.method !== 'POST') return res.status(405).end()\n\n  const ip = (req.headers['x-forwarded-for'] as string) ?? req.socket.remoteAddress ?? 'unknown'\n\n  try {\n    const { name, email, message } = req.body\n\n    if (!name || !email || !message) {\n      return res.status(400).json({ error: 'All fields are required' })\n    }\n\n    await mailer.send({\n      to: process.env.ESENDY_TO!,\n      subject: `New contact from ${name}`,\n      template: (\n        <ContactTemplate\n          senderName={name}\n          senderEmail={email}\n          message={message}\n        />\n      ),\n      rateLimitKey: ip,\n    })\n\n    return res.status(200).json({ success: true })\n  } catch (err) {\n    if (err instanceof EsendyRateLimitError) return res.status(429).json({ error: 'Too many requests' })\n    if (err instanceof EsendyValidationError) return res.status(400).json({ error: err.message })\n    console.error('[esendy] send failed:', err)\n    return res.status(500).json({ error: 'Failed to send message' })\n  }\n}\n```\n\n### Vite + Express (or Fastify)\n\n```ts\n// server/routes/contact.ts  (Express example)\nimport { Router } from 'express'\nimport { mailer } from '../lib/mailer'\nimport { ContactTemplate } from '@devstevenjs/esendy/templates'\nimport { EsendyRateLimitError, EsendyValidationError } from '@devstevenjs/esendy'\n\nconst router = Router()\n\nrouter.post('/contact', async (req, res) => {\n  const ip = req.headers['x-forwarded-for'] as string ?? req.ip ?? 'unknown'\n\n  try {\n    const { name, email, message } = req.body\n\n    if (!name || !email || !message) {\n      return res.status(400).json({ error: 'All fields are required' })\n    }\n\n    await mailer.send({\n      to: process.env.ESENDY_TO!,\n      subject: `New contact from ${name}`,\n      template: (\n        <ContactTemplate\n          senderName={name}\n          senderEmail={email}\n          message={message}\n        />\n      ),\n      rateLimitKey: ip,\n    })\n\n    return res.json({ success: true })\n  } catch (err) {\n    if (err instanceof EsendyRateLimitError) return res.status(429).json({ error: 'Too many requests' })\n    if (err instanceof EsendyValidationError) return res.status(400).json({ error: err.message })\n    console.error('[esendy] send failed:', err)\n    return res.status(500).json({ error: 'Failed to send message' })\n  }\n})\n\nexport default router\n```\n\n---\n\n## Usage: Server-Initiated Notifications\n\nFor event-driven emails (new user registered, order placed, etc.) you typically\ndon't need rate limiting. Pass `rateLimit: false` in config, or don't pass `rateLimitKey`.\n\n```ts\n// lib/mailer.ts — separate instance for system emails (no rate limit)\nexport const systemMailer = createEsendy({\n  smtp: { ... },\n  from: { name: 'My App', address: process.env.ESENDY_SMTP_USER! },\n  rateLimit: false,   // no rate limiting for server-initiated sends\n  defaults: { siteName: 'My App', primaryColor: '#2563eb' },\n})\n```\n\n```ts\n// services/user.service.ts\nimport { systemMailer } from '@/lib/mailer'\nimport { NotificationTemplate } from '@devstevenjs/esendy/templates'\n\nexport async function sendWelcomeEmail(userEmail: string, userName: string) {\n  await systemMailer.send({\n    to: userEmail,\n    subject: 'Welcome to My App',\n    template: (\n      <NotificationTemplate\n        title={`Welcome, ${userName}!`}\n        body=\"Your account has been created. You can now log in and get started.\"\n        ctaText=\"Go to Dashboard\"\n        ctaUrl=\"https://yourdomain.com/dashboard\"\n      />\n    ),\n  })\n}\n\nexport async function sendOrderConfirmation(userEmail: string, orderId: string) {\n  await systemMailer.send({\n    to: userEmail,\n    subject: `Order #${orderId} confirmed`,\n    template: (\n      <NotificationTemplate\n        title=\"Order Confirmed\"\n        body={`Your order #${orderId} has been received and is being processed.`}\n        ctaText=\"View Order\"\n        ctaUrl={`https://yourdomain.com/orders/${orderId}`}\n      />\n    ),\n  })\n}\n```\n\n---\n\n## Templates\n\n### `ContactTemplate`\n\nFor contact form submissions. Displays sender name, email, message, and optional extra fields.\n\n| Prop | Type | Required | Default |\n|------|------|----------|---------|\n| `senderName` | `string` | ✓ | — |\n| `senderEmail` | `string` | ✓ | — |\n| `message` | `string` | ✓ | — |\n| `extraFields` | `Record<string, string>` | | `undefined` |\n| `siteName` | `string` | | `'My Site'` |\n| `primaryColor` | `string` | | `'#2563eb'` |\n| `logoUrl` | `string` | | `undefined` |\n| `logoAlt` | `string` | | `siteName` |\n| `footerText` | `string` | | `'© {year} {siteName}'` |\n\n### `NotificationTemplate`\n\nFor server-initiated event emails (welcome, order, password reset, etc.).\n\n| Prop | Type | Required | Default |\n|------|------|----------|---------|\n| `title` | `string` | ✓ | — |\n| `body` | `string \\| ReactNode` | ✓ | — |\n| `previewText` | `string` | | `title` |\n| `ctaText` | `string` | | `undefined` |\n| `ctaUrl` | `string` | | `undefined` |\n| `siteName` | `string` | | `'Notification'` |\n| `primaryColor` | `string` | | `'#2563eb'` |\n| `logoUrl` | `string` | | `undefined` |\n| `logoAlt` | `string` | | `siteName` |\n| `footerText` | `string` | | `'© {year} {siteName}'` |\n\n### `Layout`\n\nThe base wrapper used by both templates. Use this to build custom templates that\nmatch the same design system.\n\n```tsx\nimport { Layout } from '@devstevenjs/esendy/templates'\nimport { Text, Button, Section } from '@react-email/components'\n\nfunction MyCustomTemplate({ userEmail }: { userEmail: string }) {\n  return (\n    <Layout siteName=\"My App\" primaryColor=\"#16a34a\" previewText=\"Your account details\">\n      <Text>Your registered email is: {userEmail}</Text>\n    </Layout>\n  )\n}\n```\n\n---\n\n## Telegram Delivery\n\nWhen a `telegram` block is present on the mailer instance, esendy automatically posts one message to a single configured Telegram group after each successful `send()`. No per-send options needed — configure once, mirrors every send.\n\n### Setup\n\n1. Talk to **@BotFather** on Telegram → `/newbot` → copy the **bot token**.\n2. Add the bot to the target group. (For private groups, disable BotFather privacy mode, or @mention the bot once so it can post.)\n3. Get the **chat ID**: add **@RawDataBot** (or `@getidsbot`) to the group briefly, or call `https://api.telegram.org/bot<token>/getUpdates` after a message in the group and read `result[].message.chat.id`. Group IDs are negative (supergroups start with `-100`).\n4. Store both values in env vars: `ESENDY_TELEGRAM_BOT_TOKEN` and `ESENDY_TELEGRAM_CHAT_ID`.\n\n### Configuration\n\n```ts\ncreateEsendy({\n  // ... smtp, from, rateLimit, defaults ...\n\n  telegram: {\n    botToken: process.env.ESENDY_TELEGRAM_BOT_TOKEN!,  // SECRET — never log\n    chatId: process.env.ESENDY_TELEGRAM_CHAT_ID!,       // the ONE target group (negative for groups)\n    enabled?: boolean,               // default: true when the block is present\n    parseMode?: 'HTML' | 'MarkdownV2' | 'None',  // default: 'HTML'\n    disableNotification?: boolean,   // default: false (silent push)\n    failureMode?: 'silent' | 'throw', // default: 'silent'\n    timeoutMs?: number,              // default: 10_000 (10s)\n    apiBaseUrl?: string,             // default: 'https://api.telegram.org' (override for tests)\n  },\n})\n```\n\n### Message format\n\nThe Telegram message is derived from the email subject and auto-generated plain-text body:\n\n```\n<b>Email subject</b>\n\nPlain-text body of the email (HTML-escaped for parseMode: 'HTML')\n```\n\n- `parseMode: 'HTML'` (default): interpolated content is HTML-escaped (`&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;`); the `<b>` subject wrapper is esendy's own markup.\n- `parseMode: 'MarkdownV2'`: subject and body are escaped per Telegram's MarkdownV2 rules; subject is wrapped in `*...*`.\n- `parseMode: 'None'`: raw text, no escaping, no parse_mode sent to Telegram.\n- Messages are capped at 4 096 characters (Telegram's limit); longer messages are truncated and end with `…`.\n\n### Behavior & failure isolation\n\n- **Email first.** Telegram is only attempted after the email is successfully sent. Validation errors, rate-limit errors, and SMTP failures propagate exactly as in v1.0.x — Telegram is never called.\n- **Silent by default.** A Telegram failure does not fail your contact form. The email is already sent; `send()` returns with `telegram: { ok: false, error: '...' }`. Use `failureMode: 'throw'` if you need strict delivery guarantees (note: the email is already sent when the error is thrown).\n- **No retries.** One attempt per send (retry support is planned for v1.2).\n- **Telegram's own rate limits.** Telegram allows ~30 messages/second to different chats and ~1 message/second to the same group. For contact forms this is well within limits, but be aware if you are sending high volumes with `systemMailer`.\n- **No new dependencies.** The Telegram call uses the native `fetch` API (Node 18+, targeting Node 24).\n\n### Inspecting the result\n\n`send()` returns an `EsendySendResult` with `email` and an optional `telegram` field:\n\n```ts\nconst result = await mailer.send({ to, subject, template, rateLimitKey })\n\n// Email outcome (always present)\nresult.email.messageId\nresult.email.accepted\nresult.email.rejected\n\n// Telegram outcome (present only when a telegram block is configured)\nresult.telegram?.ok          // true on success\nresult.telegram?.messageId   // Telegram message_id on success\nresult.telegram?.skipped     // true when enabled: false\nresult.telegram?.error       // redacted error string on failure (silent mode)\n```\n\n### Error handling\n\n```ts\nimport { EsendyTelegramError } from '@devstevenjs/esendy'\n\ntry {\n  await mailer.send({ ... })\n} catch (err) {\n  if (err instanceof EsendyTelegramError) {\n    // Only thrown when failureMode: 'throw'\n    // Note: the email was already sent when this error is raised\n    err.status          // HTTP status from Telegram API (if any)\n    err.telegramErrorCode  // error_code from Telegram response body (if any)\n    // → return 502 or log and continue\n  }\n}\n```\n\n---\n\n## Configuration Reference\n\n```ts\ncreateEsendy({\n  smtp: {\n    host: string          // SMTP hostname\n    port: number          // 587 (STARTTLS) or 465 (SSL)\n    secure?: boolean      // auto-detected from port if omitted\n    auth: {\n      user: string\n      pass: string\n    }\n  },\n\n  from: string | { name: string; address: string }\n  // string form:  '\"My App\" <no-reply@domain.com>'\n  // object form:  { name: 'My App', address: 'no-reply@domain.com' }\n\n  rateLimit?: {\n    windowMs?: number   // default: 60_000 (1 minute)\n    max?: number        // default: 5 emails per window per key\n  } | false             // false = disable rate limiting entirely\n\n  defaults?: {\n    siteName?: string\n    primaryColor?: string   // hex color\n    logoUrl?: string        // https URL only\n    logoAlt?: string\n    footerText?: string\n  }\n\n  // Optional — see \"Telegram Delivery\" section\n  telegram?: {\n    botToken: string                               // SECRET from @BotFather\n    chatId: string | number                        // target group (negative for groups)\n    enabled?: boolean                              // default: true\n    parseMode?: 'HTML' | 'MarkdownV2' | 'None'    // default: 'HTML'\n    disableNotification?: boolean                  // default: false\n    failureMode?: 'silent' | 'throw'               // default: 'silent'\n    timeoutMs?: number                             // default: 10_000\n    apiBaseUrl?: string                            // default: 'https://api.telegram.org'\n  }\n})\n```\n\n---\n\n## Error Handling\n\nEsendy throws typed errors you can catch and map to HTTP responses:\n\n```ts\nimport { EsendyRateLimitError, EsendyValidationError, EsendyTelegramError } from '@devstevenjs/esendy'\n\ntry {\n  await mailer.send({ ... })\n} catch (err) {\n  if (err instanceof EsendyRateLimitError) {\n    // err.retryAfterMs — how long until the window resets\n    // → return 429\n  }\n  if (err instanceof EsendyValidationError) {\n    // Invalid email address, empty subject, too many recipients, etc.\n    // → return 400\n  }\n  if (err instanceof EsendyTelegramError) {\n    // Only thrown when telegram.failureMode is 'throw'\n    // The email was already sent when this error is raised\n    // err.status, err.telegramErrorCode\n    // → return 502 or log and continue\n  }\n  // SMTP connectivity errors (wrong host, auth failed, etc.) are\n  // plain Error instances from nodemailer → return 500\n}\n```\n\n---\n\n## SMTP Health Check\n\nVerify connectivity on application startup:\n\n```ts\n// Next.js: instrumentation.ts (App Router)\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === 'nodejs') {\n    const { mailer } = await import('./lib/mailer')\n    await mailer.verify()\n    console.log('[esendy] SMTP connection verified')\n  }\n}\n\n// Express / Fastify: before server.listen()\nawait mailer.verify()\n```\n\n---\n\n## Graceful Shutdown\n\nCall `destroy()` so the SMTP connection and cleanup timer close cleanly:\n\n```ts\n// Express\nprocess.on('SIGTERM', () => {\n  mailer.destroy()\n  server.close()\n})\n\n// Next.js — not typically needed since Vercel/Node manages process lifecycle\n```\n\n---\n\n## Updating the Package\n\nIn consuming projects, update normally:\n\n```bash\nnpm update @devstevenjs/esendy\n# or to pin to a specific version\nnpm install @devstevenjs/esendy@1.1.0\n```\n\nUse a `^` range in `package.json` (`\"@devstevenjs/esendy\": \"^1.0.0\"`) to receive minor and patch updates automatically via `npm update`, or pin to an exact version if you prefer explicit control.\n\n---\n\n## Security Notes\n\n- **reCAPTCHA / honeypot / bot protection** — these belong on the contact form in each project, not in this package. Esendy handles the sending side only.\n- **Never log the SMTP password** — esendy disables nodemailer's built-in logger to prevent credential leaks.\n- **TLS certificate verification** is always enabled (`rejectUnauthorized: true`). Do not override this.\n- **HTML from user input** (contact form messages) is sanitized with `sanitize-html` before being embedded in templates, preventing XSS even if the email is viewed in a webmail client.\n- **Rate limiting** is in-memory and per-process. For multi-instance deployments (e.g. multiple Next.js replicas), consider a shared store (Redis) for rate limiting at the load balancer or API gateway level.\n- **Telegram bot token is a secret** — treat it like a password. Esendy never logs it; error messages and the result object are always redacted. Store it in an env var, never commit it.\n\n---\n## For AI Coding Assistants\n\nYou can use this prompt to guide a coding assistant you use to implement this package.\n\n```\nI have a public npm package called `@devstevenjs/esendy` that I want to use for all email sending in the project. Before implementing any email functionality, read its full README here: <put npm url of the package here>\n\nKey things to know before you start:\n- Install with `npm install @devstevenjs/esendy`\n- Two import paths: `from '@devstevenjs/esendy'` and `from '@devstevenjs/esendy/templates'`\n- Never use nodemailer directly — esendy wraps it with TLS enforcement, header injection prevention, HTML sanitization, email validation, and rate limiting built in\n- Always create two mailer instances in `lib/mailer.ts` (Next.js) or `src/lib/mailer.ts` (Vite): one called `mailer` with rateLimit for contact forms (keyed by client IP), one called `systemMailer` with `rateLimit: false` for server-initiated emails (welcome, order confirmation, etc.)\n- Env vars follow the `ESENDY_` prefix convention: `ESENDY_SMTP_HOST`, `ESENDY_SMTP_PORT`, `ESENDY_SMTP_USER`, `ESENDY_SMTP_PASS`, `ESENDY_FROM_NAME`, `ESENDY_TO`\n- Optional Telegram mirroring: add a single `telegram` block to `createEsendy` with `botToken` and `chatId`. When present, every successful `send()` posts one message to the configured group automatically — no per-send changes needed. Env vars: `ESENDY_TELEGRAM_BOT_TOKEN`, `ESENDY_TELEGRAM_CHAT_ID`. Gracefully no-ops when unset. Wire it on the `mailer` instance only (contact forms); `systemMailer` typically does not need it.\n- Four error types to handle: `EsendyValidationError` → 400, `EsendyRateLimitError` → 429 (has `.retryAfterMs`), plain nodemailer `Error` → 500, `EsendyTelegramError` → only thrown when `telegram.failureMode: ‘throw’` (email already sent; usually log + continue)\n- reCAPTCHA, honeypots, and bot protection not handled in the package, esendy handles sending only\n- If this project is on React 18, use `--legacy-peer-deps` when installing\n\nNow [replace the existing email logic / implement the contact form email / implement the notification emails] in this project using esendy.\n```\nIn the last line you are supposed to choose a use-case.\n\n---\n\n## Changelog\n\n### 1.1.1\n- **Fix:** `htmlToText` now preserves newlines from block-level elements and `<br>` tags; previously collapsed everything to a single line in the plain-text email fallback and Telegram messages. Also decodes named HTML entities, decimal numeric entities (`&#NNN;`), and hex numeric entities (`&#xHHH;`) without any new dependency.\n\n### 1.1.0\n- Add optional Telegram delivery: mirror each sent email to a single configured Telegram group via a bot (native `fetch`, no new dependencies). Configure once with a `telegram` block on `createEsendy`; one message is sent automatically on each successful `send()`. Email-first ordering; Telegram failures are silent by default and never block the email. New export: `EsendyTelegramError`. New types: `TelegramConfig`, `TelegramDeliveryResult`, `EsendySendResult`.\n\n### 1.0.2\n- Added AI section in Readme to help those using AI coding assistants.\n\n### 1.0.1\n- **Fix:** ESM build (`dist/index.mjs`) now resolves correctly on Node.js 24. The deep-path import `validator/lib/isEmail` was bundled without a `.js` extension, causing `ERR_MODULE_NOT_FOUND` under Node 24's strict ESM resolver. Switched to importing from the `validator` package root instead.\n\n### 1.0.0\n- Initial release.\n\n","readmeFilename":"README.md"}