243 lines
7.4 KiB
TypeScript
243 lines
7.4 KiB
TypeScript
/**
|
|
* Mailgun Email Service
|
|
* This service handles email sending via Mailgun API
|
|
*/
|
|
|
|
import { getMailgunConfig, type MailgunConfig } from './mailgun.config';
|
|
import { getAppConfig } from '../config/unified.config';
|
|
import { logger } from './logging';
|
|
import { normalizeError } from '../utils/error';
|
|
|
|
interface EmailTemplate {
|
|
subject: string;
|
|
html: string;
|
|
text?: string;
|
|
}
|
|
|
|
export class MailgunService {
|
|
private config: MailgunConfig;
|
|
private readonly context = 'MAILGUN';
|
|
|
|
constructor() {
|
|
this.config = getMailgunConfig();
|
|
|
|
// Log configuration status on startup
|
|
const status = this.getConfigurationStatus();
|
|
if (!status.configured) {
|
|
logger.warn(
|
|
'Mailgun running in development mode; emails will not be delivered',
|
|
this.context,
|
|
{
|
|
missingFields: status.missingFields,
|
|
domain: status.domain,
|
|
}
|
|
);
|
|
logger.info(
|
|
'To enable email delivery, configure Mailgun environment variables',
|
|
this.context,
|
|
{
|
|
requiredVariables: status.missingFields,
|
|
}
|
|
);
|
|
} else {
|
|
logger.info('Mailgun configured for delivery', this.context, {
|
|
domain: status.domain,
|
|
fromEmail: status.fromEmail,
|
|
});
|
|
}
|
|
}
|
|
|
|
private getVerificationEmailTemplate(verificationUrl: string): EmailTemplate {
|
|
return {
|
|
subject: 'Verify Your Email - Medication Reminder',
|
|
html: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: #4f46e5;">Verify Your Email Address</h2>
|
|
<p>Thank you for signing up for Medication Reminder! Please click the button below to verify your email address:</p>
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
<a href="${verificationUrl}"
|
|
style="background-color: #4f46e5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
Verify Email Address
|
|
</a>
|
|
</div>
|
|
<p>Or copy and paste this link into your browser:</p>
|
|
<p style="word-break: break-all; color: #6b7280;">${verificationUrl}</p>
|
|
<p style="color: #6b7280; font-size: 14px;">This link will expire in 24 hours.</p>
|
|
</div>
|
|
`,
|
|
text: `
|
|
Verify Your Email - Medication Reminder
|
|
|
|
Thank you for signing up! Please verify your email by visiting:
|
|
${verificationUrl}
|
|
|
|
This link will expire in 24 hours.
|
|
`,
|
|
};
|
|
}
|
|
|
|
private getPasswordResetEmailTemplate(resetUrl: string): EmailTemplate {
|
|
return {
|
|
subject: 'Reset Your Password - Medication Reminder',
|
|
html: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: #4f46e5;">Reset Your Password</h2>
|
|
<p>You requested to reset your password. Click the button below to set a new password:</p>
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
<a href="${resetUrl}"
|
|
style="background-color: #4f46e5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
Reset Password
|
|
</a>
|
|
</div>
|
|
<p>Or copy and paste this link into your browser:</p>
|
|
<p style="word-break: break-all; color: #6b7280;">${resetUrl}</p>
|
|
<p style="color: #6b7280; font-size: 14px;">This link will expire in 1 hour. If you didn't request this, please ignore this email.</p>
|
|
</div>
|
|
`,
|
|
text: `
|
|
Reset Your Password - Medication Reminder
|
|
|
|
You requested to reset your password. Visit this link to set a new password:
|
|
${resetUrl}
|
|
|
|
This link will expire in 1 hour. If you didn't request this, please ignore this email.
|
|
`,
|
|
};
|
|
}
|
|
|
|
async sendEmail(to: string, template: EmailTemplate): Promise<boolean> {
|
|
try {
|
|
const status = this.getConfigurationStatus();
|
|
if (!status.configured) {
|
|
logger.warn(
|
|
'Skipping email send; Mailgun is not configured',
|
|
this.context,
|
|
{
|
|
to,
|
|
subject: template.subject,
|
|
missingFields: status.missingFields,
|
|
preview: status.mode === 'development',
|
|
}
|
|
);
|
|
logger.debug('Mailgun email preview', this.context, {
|
|
to,
|
|
subject: template.subject,
|
|
html: template.html,
|
|
text: template.text,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Production Mailgun API call
|
|
const formData = new FormData();
|
|
formData.append(
|
|
'from',
|
|
`${this.config.fromName} <${this.config.fromEmail}>`
|
|
);
|
|
formData.append('to', to);
|
|
formData.append('subject', template.subject);
|
|
formData.append('html', template.html);
|
|
if (template.text) {
|
|
formData.append('text', template.text);
|
|
}
|
|
|
|
const response = await fetch(
|
|
`${this.config.baseUrl}/${this.config.domain}/messages`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Basic ${btoa(`api:${this.config.apiKey}`)}`,
|
|
},
|
|
body: formData,
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Mailgun API error: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
logger.info('Email sent via Mailgun', this.context, {
|
|
to,
|
|
subject: template.subject,
|
|
messageId: result.id,
|
|
});
|
|
|
|
return true;
|
|
} catch (error: unknown) {
|
|
logger.error(
|
|
'Mailgun email send failed',
|
|
this.context,
|
|
{
|
|
domain: this.config.domain,
|
|
},
|
|
normalizeError(error)
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async sendVerificationEmail(email: string, token: string): Promise<boolean> {
|
|
const verificationUrl = `${getAppConfig().baseUrl}/verify-email?token=${token}`;
|
|
const template = this.getVerificationEmailTemplate(verificationUrl);
|
|
return this.sendEmail(email, template);
|
|
}
|
|
|
|
async sendPasswordResetEmail(email: string, token: string): Promise<boolean> {
|
|
const resetUrl = `${getAppConfig().baseUrl}/reset-password?token=${token}`;
|
|
const template = this.getPasswordResetEmailTemplate(resetUrl);
|
|
return this.sendEmail(email, template);
|
|
}
|
|
|
|
// Get configuration status for debugging
|
|
getConfigurationStatus(): {
|
|
configured: boolean;
|
|
mode: 'development' | 'production';
|
|
domain: string;
|
|
fromEmail: string;
|
|
missingFields: string[];
|
|
} {
|
|
const missingFields = this.getMissingFields();
|
|
const configured = missingFields.length === 0;
|
|
const mode: 'development' | 'production' = configured
|
|
? 'production'
|
|
: 'development';
|
|
return {
|
|
configured,
|
|
mode,
|
|
domain: this.config.domain,
|
|
fromEmail: this.config.fromEmail,
|
|
missingFields,
|
|
};
|
|
}
|
|
|
|
private getMissingFields(): string[] {
|
|
const missing: string[] = [];
|
|
|
|
if (!this.config.apiKey) {
|
|
missing.push('VITE_MAILGUN_API_KEY');
|
|
}
|
|
|
|
if (!this.config.domain) {
|
|
missing.push('VITE_MAILGUN_DOMAIN');
|
|
}
|
|
|
|
if (!this.config.fromEmail) {
|
|
missing.push('VITE_MAILGUN_FROM_EMAIL');
|
|
}
|
|
|
|
if (!this.config.baseUrl) {
|
|
missing.push('VITE_MAILGUN_BASE_URL');
|
|
}
|
|
|
|
if (!this.config.fromName) {
|
|
missing.push('VITE_MAILGUN_FROM_NAME');
|
|
}
|
|
|
|
return missing;
|
|
}
|
|
}
|
|
|
|
export const mailgunService = new MailgunService();
|