@cinnabun/mail 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/console.adapter.d.ts +8 -0
- package/dist/adapters/console.adapter.js +26 -0
- package/dist/adapters/smtp.adapter.d.ts +19 -0
- package/dist/adapters/smtp.adapter.js +48 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/interfaces/mail-adapter.d.ts +6 -0
- package/dist/interfaces/mail-adapter.js +1 -0
- package/dist/interfaces/mail-message.d.ts +15 -0
- package/dist/interfaces/mail-message.js +1 -0
- package/dist/interfaces/mail-options.d.ts +17 -0
- package/dist/interfaces/mail-options.js +1 -0
- package/dist/mail-service-holder.d.ts +3 -0
- package/dist/mail-service-holder.js +7 -0
- package/dist/mail.module.d.ts +11 -0
- package/dist/mail.module.js +58 -0
- package/dist/mail.plugin.d.ts +7 -0
- package/dist/mail.plugin.js +29 -0
- package/dist/services/mail.service.d.ts +11 -0
- package/dist/services/mail.service.js +26 -0
- package/dist/services/template.service.d.ts +11 -0
- package/dist/services/template.service.js +48 -0
- package/dist/templates/default.html +16 -0
- package/package.json +36 -0
- package/src/templates/default.html +16 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MailMessage } from "../interfaces/mail-message.js";
|
|
2
|
+
import type { MailAdapter } from "../interfaces/mail-adapter.js";
|
|
3
|
+
export declare class ConsoleAdapter implements MailAdapter {
|
|
4
|
+
private connected;
|
|
5
|
+
connect(): Promise<void>;
|
|
6
|
+
disconnect(): Promise<void>;
|
|
7
|
+
send(message: MailMessage): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class ConsoleAdapter {
|
|
2
|
+
connected = false;
|
|
3
|
+
async connect() {
|
|
4
|
+
this.connected = true;
|
|
5
|
+
}
|
|
6
|
+
async disconnect() {
|
|
7
|
+
this.connected = false;
|
|
8
|
+
}
|
|
9
|
+
async send(message) {
|
|
10
|
+
if (!this.connected) {
|
|
11
|
+
throw new Error("ConsoleAdapter not connected. Call connect() first.");
|
|
12
|
+
}
|
|
13
|
+
const to = Array.isArray(message.to) ? message.to.join(", ") : message.to;
|
|
14
|
+
const lines = [
|
|
15
|
+
"--- Email (Console Adapter) ---",
|
|
16
|
+
`To: ${to}`,
|
|
17
|
+
message.from ? `From: ${message.from}` : null,
|
|
18
|
+
`Subject: ${message.subject}`,
|
|
19
|
+
"---",
|
|
20
|
+
message.text ? `Text:\n${message.text}` : null,
|
|
21
|
+
message.html ? `HTML:\n${message.html}` : null,
|
|
22
|
+
"---",
|
|
23
|
+
].filter(Boolean);
|
|
24
|
+
console.log(lines.join("\n"));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MailMessage } from "../interfaces/mail-message.js";
|
|
2
|
+
import type { MailAdapter } from "../interfaces/mail-adapter.js";
|
|
3
|
+
export interface SmtpAdapterConfig {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
secure?: boolean;
|
|
7
|
+
auth?: {
|
|
8
|
+
user: string;
|
|
9
|
+
pass: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare class SmtpAdapter implements MailAdapter {
|
|
13
|
+
private transporter;
|
|
14
|
+
private readonly config;
|
|
15
|
+
constructor(config: SmtpAdapterConfig);
|
|
16
|
+
connect(): Promise<void>;
|
|
17
|
+
disconnect(): Promise<void>;
|
|
18
|
+
send(message: MailMessage): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class SmtpAdapter {
|
|
2
|
+
transporter = null;
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async connect() {
|
|
8
|
+
const nodemailer = await import("nodemailer");
|
|
9
|
+
this.transporter = nodemailer.createTransport({
|
|
10
|
+
host: this.config.host,
|
|
11
|
+
port: this.config.port,
|
|
12
|
+
secure: this.config.secure ?? this.config.port === 465,
|
|
13
|
+
auth: this.config.auth,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async disconnect() {
|
|
17
|
+
if (this.transporter) {
|
|
18
|
+
this.transporter.close();
|
|
19
|
+
this.transporter = null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async send(message) {
|
|
23
|
+
if (!this.transporter) {
|
|
24
|
+
throw new Error("SmtpAdapter not connected. Call connect() first.");
|
|
25
|
+
}
|
|
26
|
+
const mailOptions = {
|
|
27
|
+
to: Array.isArray(message.to) ? message.to : [message.to],
|
|
28
|
+
from: message.from,
|
|
29
|
+
subject: message.subject,
|
|
30
|
+
text: message.text,
|
|
31
|
+
html: message.html,
|
|
32
|
+
};
|
|
33
|
+
if (message.cc) {
|
|
34
|
+
mailOptions.cc = Array.isArray(message.cc) ? message.cc : [message.cc];
|
|
35
|
+
}
|
|
36
|
+
if (message.bcc) {
|
|
37
|
+
mailOptions.bcc = Array.isArray(message.bcc) ? message.bcc : [message.bcc];
|
|
38
|
+
}
|
|
39
|
+
if (message.attachments?.length) {
|
|
40
|
+
mailOptions.attachments = message.attachments.map((a) => ({
|
|
41
|
+
filename: a.filename,
|
|
42
|
+
content: a.content,
|
|
43
|
+
contentType: a.contentType,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
await this.transporter.sendMail(mailOptions);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { MailMessage, Attachment } from "./interfaces/mail-message.js";
|
|
2
|
+
export type { MailAdapter } from "./interfaces/mail-adapter.js";
|
|
3
|
+
export type { MailModuleOptions } from "./interfaces/mail-options.js";
|
|
4
|
+
export { SmtpAdapter } from "./adapters/smtp.adapter.js";
|
|
5
|
+
export { ConsoleAdapter } from "./adapters/console.adapter.js";
|
|
6
|
+
export { MailService } from "./services/mail.service.js";
|
|
7
|
+
export { TemplateService } from "./services/template.service.js";
|
|
8
|
+
export { MailModule } from "./mail.module.js";
|
|
9
|
+
export { MailPlugin } from "./mail.plugin.js";
|
|
10
|
+
export { getMailService } from "./mail-service-holder.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SmtpAdapter } from "./adapters/smtp.adapter.js";
|
|
2
|
+
export { ConsoleAdapter } from "./adapters/console.adapter.js";
|
|
3
|
+
export { MailService } from "./services/mail.service.js";
|
|
4
|
+
export { TemplateService } from "./services/template.service.js";
|
|
5
|
+
export { MailModule } from "./mail.module.js";
|
|
6
|
+
export { MailPlugin } from "./mail.plugin.js";
|
|
7
|
+
export { getMailService } from "./mail-service-holder.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Attachment {
|
|
2
|
+
filename: string;
|
|
3
|
+
content: Buffer | string;
|
|
4
|
+
contentType?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MailMessage {
|
|
7
|
+
to: string | string[];
|
|
8
|
+
from?: string;
|
|
9
|
+
subject: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
html?: string;
|
|
12
|
+
cc?: string | string[];
|
|
13
|
+
bcc?: string | string[];
|
|
14
|
+
attachments?: Attachment[];
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface MailModuleOptions {
|
|
2
|
+
adapter: "smtp" | "console";
|
|
3
|
+
from?: string;
|
|
4
|
+
smtp?: {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
secure?: boolean;
|
|
8
|
+
auth?: {
|
|
9
|
+
user: string;
|
|
10
|
+
pass: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
templates?: {
|
|
14
|
+
dir?: string;
|
|
15
|
+
engine?: "handlebars" | "ejs";
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MailModuleOptions } from "./interfaces/mail-options.js";
|
|
2
|
+
import type { MailAdapter } from "./interfaces/mail-adapter.js";
|
|
3
|
+
import { MailPlugin } from "./mail.plugin.js";
|
|
4
|
+
export declare class MailModule {
|
|
5
|
+
static forRoot(options: MailModuleOptions): Function;
|
|
6
|
+
static getOptions(): MailModuleOptions;
|
|
7
|
+
static createAdapter(): MailAdapter;
|
|
8
|
+
static createPlugin(): MailPlugin;
|
|
9
|
+
static setService(name: string, service: unknown): void;
|
|
10
|
+
static getService<T>(name: string): T;
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Module } from "@cinnabun/core";
|
|
8
|
+
import { SmtpAdapter } from "./adapters/smtp.adapter.js";
|
|
9
|
+
import { ConsoleAdapter } from "./adapters/console.adapter.js";
|
|
10
|
+
import { MailPlugin } from "./mail.plugin.js";
|
|
11
|
+
let moduleOptions = null;
|
|
12
|
+
let _services = {};
|
|
13
|
+
export class MailModule {
|
|
14
|
+
static forRoot(options) {
|
|
15
|
+
moduleOptions = options;
|
|
16
|
+
_services = {};
|
|
17
|
+
let MailDynamicModule = class MailDynamicModule {
|
|
18
|
+
};
|
|
19
|
+
MailDynamicModule = __decorate([
|
|
20
|
+
Module({
|
|
21
|
+
imports: [],
|
|
22
|
+
controllers: [],
|
|
23
|
+
providers: [],
|
|
24
|
+
exports: [],
|
|
25
|
+
})
|
|
26
|
+
], MailDynamicModule);
|
|
27
|
+
return MailDynamicModule;
|
|
28
|
+
}
|
|
29
|
+
static getOptions() {
|
|
30
|
+
if (!moduleOptions) {
|
|
31
|
+
throw new Error("MailModule not initialized. Call MailModule.forRoot() first.");
|
|
32
|
+
}
|
|
33
|
+
return moduleOptions;
|
|
34
|
+
}
|
|
35
|
+
static createAdapter() {
|
|
36
|
+
const options = MailModule.getOptions();
|
|
37
|
+
if (options.adapter === "console") {
|
|
38
|
+
return new ConsoleAdapter();
|
|
39
|
+
}
|
|
40
|
+
if (options.adapter === "smtp" && options.smtp) {
|
|
41
|
+
return new SmtpAdapter(options.smtp);
|
|
42
|
+
}
|
|
43
|
+
throw new Error("MailModule: smtp adapter requires smtp config (host, port)");
|
|
44
|
+
}
|
|
45
|
+
static createPlugin() {
|
|
46
|
+
return new MailPlugin();
|
|
47
|
+
}
|
|
48
|
+
static setService(name, service) {
|
|
49
|
+
_services[name] = service;
|
|
50
|
+
}
|
|
51
|
+
static getService(name) {
|
|
52
|
+
const service = _services[name];
|
|
53
|
+
if (!service) {
|
|
54
|
+
throw new Error(`Service "${name}" not found. Ensure MailModule.forRoot() and MailPlugin.onInit() have been called.`);
|
|
55
|
+
}
|
|
56
|
+
return service;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CinnabunPlugin, PluginContext } from "@cinnabun/core";
|
|
2
|
+
export declare class MailPlugin implements CinnabunPlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
private adapter;
|
|
5
|
+
onInit(context: PluginContext): Promise<void>;
|
|
6
|
+
onShutdown(_context: PluginContext): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Logger } from "@cinnabun/core";
|
|
2
|
+
import { MailModule } from "./mail.module.js";
|
|
3
|
+
import { TemplateService } from "./services/template.service.js";
|
|
4
|
+
import { MailService } from "./services/mail.service.js";
|
|
5
|
+
import { setMailService } from "./mail-service-holder.js";
|
|
6
|
+
export class MailPlugin {
|
|
7
|
+
name = "MailPlugin";
|
|
8
|
+
adapter = null;
|
|
9
|
+
async onInit(context) {
|
|
10
|
+
const logger = new Logger("MailPlugin");
|
|
11
|
+
const options = MailModule.getOptions();
|
|
12
|
+
const container = context.container;
|
|
13
|
+
const adapter = MailModule.createAdapter();
|
|
14
|
+
await adapter.connect();
|
|
15
|
+
this.adapter = adapter;
|
|
16
|
+
const templateService = new TemplateService(options.templates);
|
|
17
|
+
const mailService = new MailService(adapter, templateService, options.from);
|
|
18
|
+
container.registerInstance(MailService, mailService);
|
|
19
|
+
MailModule.setService("mailService", mailService);
|
|
20
|
+
setMailService(mailService);
|
|
21
|
+
logger.info(`Mail module initialized (adapter: ${options.adapter})`);
|
|
22
|
+
}
|
|
23
|
+
async onShutdown(_context) {
|
|
24
|
+
if (this.adapter) {
|
|
25
|
+
await this.adapter.disconnect();
|
|
26
|
+
this.adapter = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MailMessage } from "../interfaces/mail-message.js";
|
|
2
|
+
import type { MailAdapter } from "../interfaces/mail-adapter.js";
|
|
3
|
+
import type { TemplateService } from "./template.service.js";
|
|
4
|
+
export declare class MailService {
|
|
5
|
+
private readonly adapter;
|
|
6
|
+
private readonly templateService;
|
|
7
|
+
private readonly defaultFrom?;
|
|
8
|
+
constructor(adapter: MailAdapter, templateService: TemplateService, defaultFrom?: string | undefined);
|
|
9
|
+
send(message: MailMessage): Promise<void>;
|
|
10
|
+
sendTemplate(to: string | string[], subject: string, templateName: string, data: Record<string, unknown>): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class MailService {
|
|
2
|
+
adapter;
|
|
3
|
+
templateService;
|
|
4
|
+
defaultFrom;
|
|
5
|
+
constructor(adapter, templateService, defaultFrom) {
|
|
6
|
+
this.adapter = adapter;
|
|
7
|
+
this.templateService = templateService;
|
|
8
|
+
this.defaultFrom = defaultFrom;
|
|
9
|
+
}
|
|
10
|
+
async send(message) {
|
|
11
|
+
const msg = {
|
|
12
|
+
...message,
|
|
13
|
+
from: message.from ?? this.defaultFrom,
|
|
14
|
+
};
|
|
15
|
+
await this.adapter.send(msg);
|
|
16
|
+
}
|
|
17
|
+
async sendTemplate(to, subject, templateName, data) {
|
|
18
|
+
const { html, text } = await this.templateService.render(templateName, data);
|
|
19
|
+
await this.send({
|
|
20
|
+
to,
|
|
21
|
+
subject,
|
|
22
|
+
html,
|
|
23
|
+
text,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MailModuleOptions } from "../interfaces/mail-options.js";
|
|
2
|
+
export declare class TemplateService {
|
|
3
|
+
private readonly templatesDir;
|
|
4
|
+
private readonly engine;
|
|
5
|
+
constructor(options?: MailModuleOptions["templates"]);
|
|
6
|
+
render(templateName: string, data: Record<string, unknown>): Promise<{
|
|
7
|
+
html?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
}>;
|
|
10
|
+
static clearCache(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
const templateCache = new Map();
|
|
4
|
+
export class TemplateService {
|
|
5
|
+
templatesDir;
|
|
6
|
+
engine;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
const dir = options?.dir ?? "templates";
|
|
9
|
+
this.templatesDir = dir.startsWith("/") || /^[A-Za-z]:/.test(dir)
|
|
10
|
+
? dir
|
|
11
|
+
: join(process.cwd(), dir);
|
|
12
|
+
this.engine = options?.engine ?? "handlebars";
|
|
13
|
+
}
|
|
14
|
+
async render(templateName, data) {
|
|
15
|
+
if (this.engine !== "handlebars") {
|
|
16
|
+
throw new Error(`Template engine "${this.engine}" not yet supported. Use "handlebars".`);
|
|
17
|
+
}
|
|
18
|
+
const Handlebars = await import("handlebars");
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const ext of [".html", ".txt"]) {
|
|
21
|
+
const filePath = join(this.templatesDir, `${templateName}${ext}`);
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(filePath, "utf-8");
|
|
24
|
+
const cacheKey = `${filePath}`;
|
|
25
|
+
let compile = templateCache.get(cacheKey);
|
|
26
|
+
if (!compile) {
|
|
27
|
+
compile = Handlebars.compile(content);
|
|
28
|
+
templateCache.set(cacheKey, compile);
|
|
29
|
+
}
|
|
30
|
+
const rendered = compile(data);
|
|
31
|
+
if (ext === ".html")
|
|
32
|
+
result.html = rendered;
|
|
33
|
+
else
|
|
34
|
+
result.text = rendered;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// File not found, skip this extension
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!result.html && !result.text) {
|
|
41
|
+
throw new Error(`Template "${templateName}" not found in ${this.templatesDir}. Expected ${templateName}.html or ${templateName}.txt`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
static clearCache() {
|
|
46
|
+
templateCache.clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>{{ title }}</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body style="font-family: sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
8
|
+
<h1>{{ title }}</h1>
|
|
9
|
+
<div>{{{ body }}}</div>
|
|
10
|
+
{{#if buttonUrl}}
|
|
11
|
+
<p style="margin-top: 24px;">
|
|
12
|
+
<a href="{{ buttonUrl }}" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">{{ buttonText }}</a>
|
|
13
|
+
</p>
|
|
14
|
+
{{/if}}
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cinnabun/mail",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Email module for Cinnabun with SMTP and template rendering",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist", "src/templates"],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"build": "tsc && cp -r src/templates dist/ 2>/dev/null || true",
|
|
15
|
+
"prepublishOnly": "bun run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["cinnabun", "mail", "email", "smtp", "nodemailer", "handlebars"],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@cinnabun/core": "^0.0.3",
|
|
20
|
+
"nodemailer": ">=6.0.0",
|
|
21
|
+
"handlebars": ">=4.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"nodemailer": { "optional": true },
|
|
25
|
+
"handlebars": { "optional": true }
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cinnabun/core": "workspace:*",
|
|
29
|
+
"@types/bun": "latest",
|
|
30
|
+
"@types/nodemailer": "^6.4.0",
|
|
31
|
+
"handlebars": "^4.7.8",
|
|
32
|
+
"nodemailer": "^6.9.0",
|
|
33
|
+
"reflect-metadata": "^0.2.2",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>{{ title }}</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body style="font-family: sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
8
|
+
<h1>{{ title }}</h1>
|
|
9
|
+
<div>{{{ body }}}</div>
|
|
10
|
+
{{#if buttonUrl}}
|
|
11
|
+
<p style="margin-top: 24px;">
|
|
12
|
+
<a href="{{ buttonUrl }}" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">{{ buttonText }}</a>
|
|
13
|
+
</p>
|
|
14
|
+
{{/if}}
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|