194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
// src/services/emailService.server.ts
|
|
/**
|
|
* @file This service manages all email sending functionality using Nodemailer.
|
|
* It is configured via environment variables and should only be used on the server.
|
|
*/
|
|
import nodemailer from 'nodemailer';
|
|
import type { Job } from 'bullmq';
|
|
import type { Logger } from 'pino';
|
|
import { logger as globalLogger } from './logger.server';
|
|
import { WatchedItemDeal } from '../types';
|
|
import type { EmailJobData } from '../types/job-data';
|
|
|
|
// 1. Create a Nodemailer transporter using SMTP configuration from environment variables.
|
|
// For development, you can use a service like Ethereal (https://ethereal.email/)
|
|
// or a local SMTP server like MailHog.
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.SMTP_HOST,
|
|
port: parseInt(process.env.SMTP_PORT || '587', 10),
|
|
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
|
|
auth: {
|
|
user: process.env.SMTP_USER,
|
|
pass: process.env.SMTP_PASS,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Sends an email using the pre-configured transporter.
|
|
* @param options The email options, including recipient, subject, and body.
|
|
*/
|
|
export const sendEmail = async (options: EmailJobData, logger: Logger) => {
|
|
const mailOptions = {
|
|
from: `"Flyer Crawler" <${process.env.SMTP_FROM_EMAIL}>`, // sender address
|
|
to: options.to,
|
|
subject: options.subject,
|
|
text: options.text,
|
|
html: options.html,
|
|
};
|
|
|
|
const info = await transporter.sendMail(mailOptions);
|
|
logger.info(
|
|
{ to: options.to, subject: options.subject, messageId: info.messageId },
|
|
`Email sent successfully.`,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Processes an email sending job from the queue.
|
|
* This is the entry point for the email worker.
|
|
* It encapsulates logging and error handling for the job.
|
|
* @param job The BullMQ job object.
|
|
*/
|
|
export const processEmailJob = async (job: Job<EmailJobData>) => {
|
|
const jobLogger = globalLogger.child({
|
|
jobId: job.id,
|
|
jobName: job.name,
|
|
recipient: job.data.to,
|
|
});
|
|
|
|
jobLogger.info(`Picked up email job.`);
|
|
|
|
try {
|
|
await sendEmail(job.data, jobLogger);
|
|
} catch (error) {
|
|
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
|
jobLogger.error(
|
|
{ err: wrappedError, jobData: job.data, attemptsMade: job.attemptsMade },
|
|
`Email job failed.`,
|
|
);
|
|
throw wrappedError;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sends a notification email to a user about new deals on their watched items.
|
|
* This function formats the deal information into a user-friendly email.
|
|
* @param to The recipient's email address.
|
|
* @param name The recipient's name (can be null).
|
|
* @param deals An array of deals found for the user.
|
|
*/
|
|
export const sendDealNotificationEmail = async (
|
|
to: string,
|
|
name: string | null,
|
|
deals: WatchedItemDeal[],
|
|
logger: Logger,
|
|
) => {
|
|
const recipientName = name || 'there';
|
|
const subject = `New Deals Found on Your Watched Items!`;
|
|
|
|
// Generate a simple list of deals for the email body
|
|
const dealsListHtml = deals
|
|
.map(
|
|
(deal) =>
|
|
`<li>
|
|
<strong>${deal.item_name}</strong> is on sale for
|
|
<strong>$${(deal.best_price_in_cents / 100).toFixed(2)}</strong>
|
|
at ${deal.store.name}!
|
|
</li>`,
|
|
)
|
|
.join('');
|
|
|
|
const html = `
|
|
<h1>Hi ${recipientName},</h1>
|
|
<p>We found some great deals on items you're watching:</p>
|
|
<ul>
|
|
${dealsListHtml}
|
|
</ul>
|
|
<p>Check them out on the deals page!</p>
|
|
`;
|
|
|
|
const text = `Hi ${recipientName},\n\nWe found some great deals on items you're watching. Visit the deals page on the site to learn more.\n\nFlyer Crawler`;
|
|
|
|
try {
|
|
// Use the generic sendEmail function to send the composed email
|
|
await sendEmail(
|
|
{
|
|
to,
|
|
subject,
|
|
text,
|
|
html,
|
|
},
|
|
logger,
|
|
);
|
|
} catch (err) {
|
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
logger.error({ err: error, to, subject }, 'Failed to send email.');
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sends a password reset email to a user containing a link with their reset token.
|
|
* @param to The recipient's email address.
|
|
* @param token The unique password reset token.
|
|
*/
|
|
export const sendPasswordResetEmail = async (to: string, token: string, logger: Logger) => {
|
|
const subject = 'Your Password Reset Request';
|
|
// Construct the full reset URL using the frontend base URL from environment variables.
|
|
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
|
|
|
|
const html = `
|
|
<div style="font-family: sans-serif; padding: 20px;">
|
|
<h2>Password Reset Request</h2>
|
|
<p>You requested a password reset for your Flyer Crawler account.</p>
|
|
<p>Please click the link below to set a new password. This link will expire in 1 hour.</p>
|
|
<a href="${resetUrl}" style="background-color: #007bff; color: white; padding: 14px 25px; text-align: center; text-decoration: none; display: inline-block; border-radius: 5px;">Reset Your Password</a>
|
|
<p style="margin-top: 20px;">If you did not request this, please ignore this email.</p>
|
|
</div>
|
|
`;
|
|
|
|
const text = `Password Reset Request\n\nYou requested a password reset for your Flyer Crawler account.\nPlease use the following link to set a new password: ${resetUrl}\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this email.`;
|
|
|
|
// Use the generic sendEmail function to send the composed email.
|
|
await sendEmail(
|
|
{
|
|
to,
|
|
subject,
|
|
text,
|
|
html,
|
|
},
|
|
logger,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Sends a welcome email to a new user.
|
|
* @param to The recipient's email address.
|
|
* @param name The recipient's name (can be null).
|
|
*/
|
|
export const sendWelcomeEmail = async (to: string, name: string | null, logger: Logger) => {
|
|
const recipientName = name || 'there';
|
|
const subject = 'Welcome to Flyer Crawler!';
|
|
|
|
const html = `
|
|
<div style="font-family: sans-serif; padding: 20px;">
|
|
<h2>Welcome!</h2>
|
|
<p>Hello ${recipientName},</p>
|
|
<p>Thank you for joining Flyer Crawler. We're excited to have you on board.</p>
|
|
<p>Start by uploading your first flyer to see how much you can save!</p>
|
|
</div>
|
|
`;
|
|
|
|
const text = `Welcome!\n\nHello ${recipientName},\n\nThank you for joining Flyer Crawler. We're excited to have you on board.\n\nStart by uploading your first flyer to see how much you can save!`;
|
|
|
|
await sendEmail(
|
|
{
|
|
to,
|
|
subject,
|
|
text,
|
|
html,
|
|
},
|
|
logger,
|
|
);
|
|
};
|