feat: Add AWS SES v2 email service

Complete transactional email layer for Elmeg:
- 3 SES templates (verification, password reset, security alert)
- TypeScript integration module with AWS SDK v3
- Template deployment script
- Usage examples
- Comprehensive README with compliance notes
This commit is contained in:
fullsizemalt 2025-12-21 16:04:59 -08:00
parent da5b5e7c45
commit 530f217445
7 changed files with 1131 additions and 0 deletions

147
email/README.md Normal file
View file

@ -0,0 +1,147 @@
# Elmeg Email Service
Transactional email layer for Elmeg using Amazon SES v2 with stored templates.
## Purpose
This module provides a production-ready email service for user-initiated transactional emails:
- **Email Verification** Sent after user registration
- **Password Reset** Sent when user requests password recovery
- **Security Alerts** Sent for account security events (new logins, password changes)
> **Compliance Note:** This service is strictly for transactional, user-initiated emails. No newsletters, marketing emails, or cold outreach. No purchased or third-party email lists are used.
## Requirements
- Node.js >= 18.0.0
- AWS account with SES verified domain
- SES templates deployed (see below)
## Environment Variables
```bash
# Required
AWS_ACCESS_KEY_ID=AKIA... # IAM user with SES permissions
AWS_SECRET_ACCESS_KEY=... # IAM user secret key
AWS_SES_REGION=us-east-1 # SES region (domain must be verified here)
EMAIL_FROM=noreply@elmeg.xyz # Verified sender address
# Optional
FRONTEND_URL=https://elmeg.xyz # For generating email links
SUPPORT_EMAIL=support@elmeg.xyz # Contact email in templates
```
## Installation
```bash
cd email
npm install
npm run build
```
## Deploy Templates to SES
Before sending emails, deploy the templates to AWS SES:
```bash
npm run deploy-templates
```
This creates/updates three templates in SES:
- `ELMEG_EMAIL_VERIFICATION`
- `ELMEG_PASSWORD_RESET`
- `ELMEG_SECURITY_ALERT`
## Usage
```typescript
import {
sendVerificationEmail,
sendPasswordResetEmail,
sendSecurityAlertEmail,
generateVerificationLink,
generateResetLink,
} from "@elmeg/email-service";
// After user registration
await sendVerificationEmail({
to: "user@example.com",
userName: "John",
verificationLink: generateVerificationLink(token),
});
// After password reset request
await sendPasswordResetEmail({
to: "user@example.com",
userName: "John",
resetLink: generateResetLink(token),
});
// After suspicious login
await sendSecurityAlertEmail({
to: "user@example.com",
userName: "John",
securityEventDescription: "New sign-in from Chrome on Windows at 10:30 AM",
});
```
## Template Placeholders
| Placeholder | Description | Templates |
|-------------|-------------|-----------|
| `{{app_name}}` | "Elmeg" | All |
| `{{user_name}}` | User's name or email prefix | All |
| `{{support_email}}` | Support contact | All |
| `{{verification_link}}` | Email verification URL | Verification |
| `{{reset_link}}` | Password reset URL | Password Reset |
| `{{security_event_description}}` | Event details | Security Alert |
## AWS SES Setup Checklist
1. **Verify Domain** Add `elmeg.xyz` in SES console with DKIM records
2. **Request Production Access** Move out of sandbox to send to any address
3. **Create IAM User** With `ses:SendEmail` and `ses:SendTemplatedEmail` permissions
4. **Deploy Templates** Run `npm run deploy-templates`
## Compliance & Best Practices
| Requirement | Implementation |
|-------------|----------------|
| User-initiated only | All emails triggered by user actions |
| No purchased lists | Only registered users receive emails |
| Bounce handling | SES automatically suppresses bounced addresses |
| Complaint handling | SES suppresses addresses that report spam |
| Unsubscribe | N/A for transactional (required action emails) |
## Error Handling
All send functions return a structured result:
```typescript
interface EmailResult {
success: boolean;
messageId?: string; // SES message ID on success
error?: {
code: string; // Error code from SES
message: string; // Human-readable error message
};
}
```
## File Structure
```
email/
├── src/
│ ├── email-service.ts # Main service module
│ └── examples.ts # Usage examples
├── scripts/
│ └── deploy-templates.ts # Template deployment script
├── templates/
│ └── README.md # Template documentation
├── package.json
├── tsconfig.json
└── README.md # This file
```

33
email/package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "@elmeg/email-service",
"version": "1.0.0",
"description": "Transactional email service for Elmeg using AWS SES v2",
"main": "dist/email-service.js",
"types": "dist/email-service.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"deploy-templates": "ts-node scripts/deploy-templates.ts"
},
"keywords": [
"email",
"ses",
"aws",
"transactional"
],
"author": "Elmeg",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-sesv2": "^3.478.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"ts-node": "^10.9.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View file

@ -0,0 +1,234 @@
/**
* Deploy SES Templates to AWS
*
* Run this script to create or update the email templates in AWS SES.
* Usage: npx ts-node scripts/deploy-templates.ts
*/
import { SESv2Client, CreateEmailTemplateCommand, UpdateEmailTemplateCommand } from "@aws-sdk/client-sesv2";
const sesClient = new SESv2Client({
region: process.env.AWS_SES_REGION || "us-east-1",
});
const APP_NAME = "Elmeg";
const SUPPORT_EMAIL = "support@elmeg.xyz";
// Template definitions
const templates = [
{
TemplateName: "ELMEG_EMAIL_VERIFICATION",
TemplateContent: {
Subject: "Verify your Elmeg account",
Html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Verify your email address</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Thanks for signing up for {{app_name}}. Please verify your email address by clicking the button below.</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #4f46e5; border-radius: 6px;">
<a href="{{verification_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Verify Email Address</a>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">This link will expire after 24 hours for your security.</p>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">If you did not create an account, you can safely ignore this email.</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="margin: 0; color: #999999; font-size: 12px;">If the button above doesn't work, copy and paste this URL: {{verification_link}}</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} Contact: {{support_email}}</p>
</td>
</tr>
</table>
</body>
</html>`,
Text: `Verify your Elmeg account
Hi {{user_name}},
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the link below:
{{verification_link}}
This link will expire after 24 hours for your security.
If you did not create an account, you can safely ignore this email.
---
{{app_name}} Contact: {{support_email}}`,
},
},
{
TemplateName: "ELMEG_PASSWORD_RESET",
TemplateContent: {
Subject: "Reset your Elmeg password",
Html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Reset your password</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">We received a request to reset the password for your {{app_name}} account. Click the button below to choose a new password.</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #4f46e5; border-radius: 6px;">
<a href="{{reset_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Reset Password</a>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">This link will expire after 1 hour for your security.</p>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">If you did not request a password reset, you can safely ignore this email.</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="margin: 0; color: #999999; font-size: 12px;">If the button above doesn't work, copy and paste this URL: {{reset_link}}</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} Contact: {{support_email}}</p>
</td>
</tr>
</table>
</body>
</html>`,
Text: `Reset your Elmeg password
Hi {{user_name}},
We received a request to reset the password for your {{app_name}} account.
Click the link below to choose a new password:
{{reset_link}}
This link will expire after 1 hour for your security.
If you did not request a password reset, you can safely ignore this email.
---
{{app_name}} Contact: {{support_email}}`,
},
},
{
TemplateName: "ELMEG_SECURITY_ALERT",
TemplateContent: {
Subject: "Security alert for your Elmeg account",
Html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<table role="presentation" cellspacing="0" cellpadding="0" style="margin-bottom: 20px;">
<tr>
<td style="background-color: #fef3c7; border-radius: 6px; padding: 12px 16px;">
<p style="margin: 0; color: #92400e; font-size: 14px; font-weight: 600;"> Security Notice</p>
</td>
</tr>
</table>
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Account activity detected</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">Hi {{user_name}},</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">We detected the following activity on your {{app_name}} account:</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 20px 0; width: 100%;">
<tr>
<td style="background-color: #f3f4f6; border-radius: 6px; padding: 16px;">
<p style="margin: 0; color: #374151; font-size: 15px; line-height: 1.5;">{{security_event_description}}</p>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">If this was you, no further action is needed.</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">If you did not perform this action, please secure your account immediately.</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0; color: #999999; font-size: 12px;">{{app_name}} Contact: {{support_email}}</p>
</td>
</tr>
</table>
</body>
</html>`,
Text: `Security alert for your Elmeg account
Hi {{user_name}},
We detected the following activity on your {{app_name}} account:
{{security_event_description}}
If this was you, no further action is needed.
If you did not perform this action, please secure your account immediately.
---
{{app_name}} Contact: {{support_email}}`,
},
},
];
async function deployTemplates() {
console.log("🚀 Deploying SES email templates...\n");
for (const template of templates) {
try {
// Try to create the template first
const createCommand = new CreateEmailTemplateCommand(template);
await sesClient.send(createCommand);
console.log(`✅ Created template: ${template.TemplateName}`);
} catch (error: unknown) {
const err = error as { name?: string };
if (err.name === "AlreadyExistsException") {
// Template exists, update it
try {
const updateCommand = new UpdateEmailTemplateCommand(template);
await sesClient.send(updateCommand);
console.log(`🔄 Updated template: ${template.TemplateName}`);
} catch (updateError) {
console.error(`❌ Failed to update ${template.TemplateName}:`, updateError);
}
} else {
console.error(`❌ Failed to create ${template.TemplateName}:`, error);
}
}
}
console.log("\n✅ Template deployment complete!");
}
deployTemplates().catch(console.error);

209
email/src/email-service.ts Normal file
View file

@ -0,0 +1,209 @@
/**
* Elmeg Email Service - AWS SES v2 Integration
*
* Transactional email layer for user-initiated emails only.
* Uses AWS SES stored templates for consistent, reliable delivery.
*/
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
// Configuration from environment variables
const config = {
region: process.env.AWS_SES_REGION || "us-east-1",
fromAddress: process.env.EMAIL_FROM || "noreply@elmeg.xyz",
appName: "Elmeg",
supportEmail: process.env.SUPPORT_EMAIL || "support@elmeg.xyz",
frontendUrl: process.env.FRONTEND_URL || "https://elmeg.xyz",
};
// SES Template Names
export const SES_TEMPLATES = {
EMAIL_VERIFICATION: "ELMEG_EMAIL_VERIFICATION",
PASSWORD_RESET: "ELMEG_PASSWORD_RESET",
SECURITY_ALERT: "ELMEG_SECURITY_ALERT",
} as const;
// Initialize SES v2 client
const sesClient = new SESv2Client({
region: config.region,
// Credentials are loaded automatically from env vars:
// AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
});
// =============================================================================
// Types
// =============================================================================
export interface SendVerificationEmailParams {
to: string;
userName: string;
verificationLink: string;
}
export interface SendPasswordResetEmailParams {
to: string;
userName: string;
resetLink: string;
}
export interface SendSecurityAlertEmailParams {
to: string;
userName: string;
securityEventDescription: string;
}
export interface EmailResult {
success: boolean;
messageId?: string;
error?: {
code: string;
message: string;
};
}
export class EmailError extends Error {
code: string;
constructor(code: string, message: string) {
super(message);
this.name = "EmailError";
this.code = code;
}
}
// =============================================================================
// Email Sending Functions
// =============================================================================
/**
* Send email verification email to new users
*/
export async function sendVerificationEmail(
params: SendVerificationEmailParams
): Promise<EmailResult> {
const templateData = {
user_name: params.userName,
verification_link: params.verificationLink,
app_name: config.appName,
support_email: config.supportEmail,
};
return sendTemplatedEmail(
params.to,
SES_TEMPLATES.EMAIL_VERIFICATION,
templateData
);
}
/**
* Send password reset email
*/
export async function sendPasswordResetEmail(
params: SendPasswordResetEmailParams
): Promise<EmailResult> {
const templateData = {
user_name: params.userName,
reset_link: params.resetLink,
app_name: config.appName,
support_email: config.supportEmail,
};
return sendTemplatedEmail(
params.to,
SES_TEMPLATES.PASSWORD_RESET,
templateData
);
}
/**
* Send security alert email for account events
*/
export async function sendSecurityAlertEmail(
params: SendSecurityAlertEmailParams
): Promise<EmailResult> {
const templateData = {
user_name: params.userName,
security_event_description: params.securityEventDescription,
app_name: config.appName,
support_email: config.supportEmail,
};
return sendTemplatedEmail(
params.to,
SES_TEMPLATES.SECURITY_ALERT,
templateData
);
}
// =============================================================================
// Core Email Function
// =============================================================================
async function sendTemplatedEmail(
to: string,
templateName: string,
templateData: Record<string, string>
): Promise<EmailResult> {
try {
const command = new SendEmailCommand({
FromEmailAddress: config.fromAddress,
Destination: {
ToAddresses: [to],
},
Content: {
Template: {
TemplateName: templateName,
TemplateData: JSON.stringify(templateData),
},
},
});
const response = await sesClient.send(command);
return {
success: true,
messageId: response.MessageId,
};
} catch (error: unknown) {
const err = error as { name?: string; message?: string; Code?: string };
console.error(`[Email] Failed to send ${templateName} to ${to}:`, err.message);
return {
success: false,
error: {
code: err.Code || err.name || "UNKNOWN_ERROR",
message: err.message || "Failed to send email",
},
};
}
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Generate a verification link for a user
*/
export function generateVerificationLink(token: string): string {
return `${config.frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
}
/**
* Generate a password reset link for a user
*/
export function generateResetLink(token: string): string {
return `${config.frontendUrl}/reset-password?token=${encodeURIComponent(token)}`;
}
/**
* Check if the email service is properly configured
*/
export function isEmailConfigured(): boolean {
return !!(
process.env.AWS_ACCESS_KEY_ID &&
process.env.AWS_SECRET_ACCESS_KEY &&
process.env.AWS_SES_REGION
);
}

176
email/src/examples.ts Normal file
View file

@ -0,0 +1,176 @@
/**
* Elmeg Email Service - Usage Examples
*
* These examples show how to integrate the email service
* into your application's user flows.
*/
import {
sendVerificationEmail,
sendPasswordResetEmail,
sendSecurityAlertEmail,
generateVerificationLink,
generateResetLink,
isEmailConfigured,
} from "./email-service";
// =============================================================================
// Example 1: User Registration Flow
// =============================================================================
async function handleUserRegistration(
userEmail: string,
userName: string,
verificationToken: string
) {
// Check if email is configured
if (!isEmailConfigured()) {
console.warn("[Email] Email service not configured, skipping verification email");
return;
}
// Generate the verification link
const verificationLink = generateVerificationLink(verificationToken);
// Send the verification email
const result = await sendVerificationEmail({
to: userEmail,
userName: userName || userEmail.split("@")[0],
verificationLink,
});
if (result.success) {
console.log(`[Email] Verification email sent to ${userEmail}, messageId: ${result.messageId}`);
} else {
console.error(`[Email] Failed to send verification email: ${result.error?.message}`);
// Handle error - maybe retry or alert admin
}
}
// =============================================================================
// Example 2: Forgot Password Flow
// =============================================================================
async function handleForgotPassword(
userEmail: string,
userName: string,
resetToken: string
) {
if (!isEmailConfigured()) {
console.warn("[Email] Email service not configured, skipping password reset email");
return;
}
const resetLink = generateResetLink(resetToken);
const result = await sendPasswordResetEmail({
to: userEmail,
userName: userName || userEmail.split("@")[0],
resetLink,
});
if (result.success) {
console.log(`[Email] Password reset email sent to ${userEmail}`);
} else {
console.error(`[Email] Failed to send password reset email: ${result.error?.message}`);
}
}
// =============================================================================
// Example 3: Security Alert - New Login
// =============================================================================
async function handleNewLogin(
userEmail: string,
userName: string,
loginDetails: { ip: string; browser: string; location?: string; timestamp: Date }
) {
if (!isEmailConfigured()) {
return;
}
const eventDescription = [
`New sign-in to your account`,
``,
`Time: ${loginDetails.timestamp.toLocaleString()}`,
`IP Address: ${loginDetails.ip}`,
`Browser: ${loginDetails.browser}`,
loginDetails.location ? `Location: ${loginDetails.location}` : null,
]
.filter(Boolean)
.join("\n");
const result = await sendSecurityAlertEmail({
to: userEmail,
userName: userName || userEmail.split("@")[0],
securityEventDescription: eventDescription,
});
if (!result.success) {
console.error(`[Email] Failed to send security alert: ${result.error?.message}`);
}
}
// =============================================================================
// Example 4: Security Alert - Password Changed
// =============================================================================
async function handlePasswordChanged(
userEmail: string,
userName: string,
timestamp: Date
) {
if (!isEmailConfigured()) {
return;
}
const eventDescription = `Your password was changed on ${timestamp.toLocaleString()}. If you did not make this change, please contact support immediately.`;
await sendSecurityAlertEmail({
to: userEmail,
userName: userName || userEmail.split("@")[0],
securityEventDescription: eventDescription,
});
}
// =============================================================================
// Example 5: Express.js Route Handler Integration
// =============================================================================
/*
import express from "express";
import { sendVerificationEmail, generateVerificationLink } from "./email-service";
const router = express.Router();
router.post("/register", async (req, res) => {
const { email, password, name } = req.body;
// ... create user in database ...
const user = await createUser({ email, password, name });
// Generate verification token
const verificationToken = generateSecureToken();
await saveVerificationToken(user.id, verificationToken);
// Send verification email
const verificationLink = generateVerificationLink(verificationToken);
const emailResult = await sendVerificationEmail({
to: email,
userName: name || email.split("@")[0],
verificationLink,
});
if (!emailResult.success) {
console.error("Failed to send verification email:", emailResult.error);
// Don't fail registration, just log the error
}
res.status(201).json({
message: "Account created. Please check your email to verify your account."
});
});
export default router;
*/

306
email/templates/README.md Normal file
View file

@ -0,0 +1,306 @@
# AWS SES Email Templates for Elmeg
## Template 1: Email Verification
**Template Name:** `ELMEG_EMAIL_VERIFICATION`
### Subject
```
Verify your Elmeg account
```
### HTML Body
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify your email</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Verify your email address</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
Hi {{user_name}},
</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the button below.
</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #4f46e5; border-radius: 6px;">
<a href="{{verification_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Verify Email Address</a>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
This link will expire after 24 hours for your security.
</p>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
If you did not create an account, you can safely ignore this email.
</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
If the button above doesn't work, copy and paste this URL into your browser:
</p>
<p style="margin: 10px 0 0; color: #4f46e5; font-size: 12px; word-break: break-all;">
{{verification_link}}
</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
{{app_name}} The Goose Community Archive
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
Questions? Contact us at {{support_email}}
</p>
</td>
</tr>
</table>
</body>
</html>
```
### Plain Text Body
```
Verify your Elmeg account
Hi {{user_name}},
Thanks for signing up for {{app_name}}. Please verify your email address by clicking the link below:
{{verification_link}}
This link will expire after 24 hours for your security.
If you did not create an account, you can safely ignore this email.
---
{{app_name}} The Goose Community Archive
Questions? Contact us at {{support_email}}
```
---
## Template 2: Password Reset
**Template Name:** `ELMEG_PASSWORD_RESET`
### Subject
```
Reset your Elmeg password
```
### HTML Body
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset your password</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Reset your password</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
Hi {{user_name}},
</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
We received a request to reset the password for your {{app_name}} account. Click the button below to choose a new password.
</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 30px 0;">
<tr>
<td style="background-color: #4f46e5; border-radius: 6px;">
<a href="{{reset_link}}" style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-size: 16px; font-weight: 600;">Reset Password</a>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
This link will expire after 1 hour for your security.
</p>
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.5;">
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
If the button above doesn't work, copy and paste this URL into your browser:
</p>
<p style="margin: 10px 0 0; color: #4f46e5; font-size: 12px; word-break: break-all;">
{{reset_link}}
</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
{{app_name}} The Goose Community Archive
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
Questions? Contact us at {{support_email}}
</p>
</td>
</tr>
</table>
</body>
</html>
```
### Plain Text Body
```
Reset your Elmeg password
Hi {{user_name}},
We received a request to reset the password for your {{app_name}} account.
Click the link below to choose a new password:
{{reset_link}}
This link will expire after 1 hour for your security.
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
---
{{app_name}} The Goose Community Archive
Questions? Contact us at {{support_email}}
```
---
## Template 3: Security Alert
**Template Name:** `ELMEG_SECURITY_ALERT`
### Subject
```
Security alert for your Elmeg account
```
### HTML Body
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Alert</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
<tr>
<td style="padding: 40px 30px; text-align: center; background-color: #1a1a2e;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 700;">{{app_name}}</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<table role="presentation" cellspacing="0" cellpadding="0" style="margin-bottom: 20px;">
<tr>
<td style="background-color: #fef3c7; border-radius: 6px; padding: 12px 16px;">
<p style="margin: 0; color: #92400e; font-size: 14px; font-weight: 600;">⚠️ Security Notice</p>
</td>
</tr>
</table>
<h2 style="margin: 0 0 20px; color: #1a1a2e; font-size: 22px; font-weight: 600;">Account activity detected</h2>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
Hi {{user_name}},
</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
We detected the following activity on your {{app_name}} account:
</p>
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 20px 0; width: 100%;">
<tr>
<td style="background-color: #f3f4f6; border-radius: 6px; padding: 16px;">
<p style="margin: 0; color: #374151; font-size: 15px; line-height: 1.5;">
{{security_event_description}}
</p>
</td>
</tr>
</table>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
If this was you, no further action is needed.
</p>
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.5;">
If you did not perform this action, we recommend you secure your account immediately by changing your password.
</p>
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 30px 0;">
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
This is an automated security notification. If you have concerns about your account security, please contact us at {{support_email}}.
</p>
</td>
</tr>
<tr>
<td style="padding: 30px; background-color: #f9fafb; text-align: center;">
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
{{app_name}} The Goose Community Archive
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
Questions? Contact us at {{support_email}}
</p>
</td>
</tr>
</table>
</body>
</html>
```
### Plain Text Body
```
Security alert for your Elmeg account
Hi {{user_name}},
We detected the following activity on your {{app_name}} account:
{{security_event_description}}
If this was you, no further action is needed.
If you did not perform this action, we recommend you secure your account immediately by changing your password.
---
This is an automated security notification. If you have concerns about your account security, please contact us at {{support_email}}.
{{app_name}} The Goose Community Archive
```
---
## Template Placeholders Reference
| Placeholder | Description | Used In |
|-------------|-------------|---------|
| `{{app_name}}` | Application name ("Elmeg") | All templates |
| `{{user_name}}` | User's display name or email | All templates |
| `{{support_email}}` | Support contact email | All templates |
| `{{verification_link}}` | Email verification URL | Email Verification |
| `{{reset_link}}` | Password reset URL | Password Reset |
| `{{security_event_description}}` | Description of the security event | Security Alert |

26
email/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": [
"ES2022"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}