end-to-end CI to match containerization on rest of adipu_server
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
This commit is contained in:
280
mailer.js
280
mailer.js
@@ -1,18 +1,29 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');
|
||||
const FormData = require('form-data');
|
||||
const Mailgun = require('mailgun.js');
|
||||
|
||||
const sesClient = new SESv2Client({
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
const mailgun = new Mailgun(FormData);
|
||||
const mg = process.env.MAILGUN_API_KEY ? mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY,
|
||||
// EU customers: set MAILGUN_API_URL=https://api.eu.mailgun.net
|
||||
url: process.env.MAILGUN_API_URL || 'https://api.mailgun.net',
|
||||
}) : null;
|
||||
|
||||
// Create the transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
SES: { sesClient, SendEmailCommand },
|
||||
});
|
||||
const DOMAIN = process.env.MAILGUN_DOMAIN;
|
||||
const FROM = process.env.EMAIL_FROM || (DOMAIN ? `BlindMaster <postmaster@${DOMAIN}>` : null);
|
||||
|
||||
async function sendMail({ to, subject, html }) {
|
||||
if (!mg || !DOMAIN) {
|
||||
console.warn('[mailer] Mailgun not configured (MAILGUN_API_KEY / MAILGUN_DOMAIN missing). Skipping send:', subject);
|
||||
return null;
|
||||
}
|
||||
return mg.messages.create(DOMAIN, {
|
||||
from: `"BlindMaster" <${FROM}>`,
|
||||
to: [to],
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get color based on time of day
|
||||
// hour parameter should be the local hour (0-23) from the client
|
||||
@@ -32,11 +43,10 @@ function getColorForTime(hour) {
|
||||
// Helper function to send email
|
||||
async function sendVerificationEmail(toEmail, token, name, localHour = new Date().getHours()) {
|
||||
const primaryColor = getColorForTime(localHour);
|
||||
const verificationLink = `https://wahwa.com/verify-email?token=${token}`;
|
||||
const verificationLink = `https://blindmaster.wahwa.com/verify-email?token=${token}`;
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: `"BlindMaster" <${process.env.EMAIL_FROM}>`, // Sender address
|
||||
const info = await sendMail({
|
||||
to: toEmail,
|
||||
subject: "Verify your BlindMaster account",
|
||||
html: `
|
||||
@@ -52,7 +62,7 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 600px;">
|
||||
|
||||
|
||||
<!-- Header with brand color -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: ${primaryColor}; padding: 40px 20px;">
|
||||
@@ -60,7 +70,7 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 14px; opacity: 0.95;">Smart Home Automation</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Welcome message -->
|
||||
<tr>
|
||||
<td style="padding: 50px 40px 30px 40px; text-align: center;">
|
||||
@@ -72,24 +82,24 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- CTA Button -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 0 40px 40px 40px;">
|
||||
<a href="${verificationLink}"
|
||||
<a href="${verificationLink}"
|
||||
style="display: inline-block; padding: 16px 48px; background-color: ${primaryColor}; color: #ffffff; text-decoration: none; border-radius: 8px; font-size: 16px; font-weight: bold; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.3s ease;">
|
||||
Verify Email Address
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="border-top: 1px solid #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer info -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px; text-align: center;">
|
||||
@@ -101,7 +111,7 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer bar -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: #f9f9f9; padding: 25px 40px;">
|
||||
@@ -110,7 +120,7 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,10 +129,10 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
console.log("Email sent successfully:", info.messageId);
|
||||
console.log("Verification email sent:", info?.id || '(skipped)');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
console.error("Error sending verification email:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -130,10 +140,9 @@ async function sendVerificationEmail(toEmail, token, name, localHour = new Date(
|
||||
// Helper function to send password reset email
|
||||
async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date().getHours()) {
|
||||
const primaryColor = getColorForTime(localHour);
|
||||
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: `"BlindMaster" <${process.env.EMAIL_FROM}>`,
|
||||
const info = await sendMail({
|
||||
to: toEmail,
|
||||
subject: "Reset your BlindMaster password",
|
||||
html: `
|
||||
@@ -149,7 +158,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 600px;">
|
||||
|
||||
|
||||
<!-- Header with brand color -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: ${primaryColor}; padding: 40px 20px;">
|
||||
@@ -157,7 +166,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 14px; opacity: 0.95;">Smart Home Automation</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Message -->
|
||||
<tr>
|
||||
<td style="padding: 50px 40px 30px 40px; text-align: center;">
|
||||
@@ -169,7 +178,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Code Display -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 0 40px 40px 40px;">
|
||||
@@ -180,14 +189,14 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="border-top: 1px solid #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer info -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px; text-align: center;">
|
||||
@@ -199,7 +208,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer bar -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: #f9f9f9; padding: 25px 40px;">
|
||||
@@ -208,7 +217,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -217,7 +226,7 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
console.log("Password reset email sent successfully:", info.messageId);
|
||||
console.log("Password reset email sent:", info?.id || '(skipped)');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error sending password reset email:", error);
|
||||
@@ -225,99 +234,13 @@ async function sendPasswordResetEmail(toEmail, code, name, localHour = new Date(
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to generate styled HTML response for verification pages
|
||||
function generateVerificationPageHTML(title, message, isSuccess = true) {
|
||||
const primaryColor = '#2196F3'; // Blue theme
|
||||
const icon = isSuccess ? '✓' : '✕';
|
||||
const iconBg = isSuccess ? primaryColor : '#f44336';
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css2?family=ABeeZee:ital@0;1&display=swap" rel="stylesheet">
|
||||
<title>${title} - BlindMaster</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f5f5f5; font-family: 'ABeeZee', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 40px 20px; min-height: 100vh;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 600px;">
|
||||
|
||||
<!-- Header with brand color -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: ${primaryColor}; padding: 40px 20px;">
|
||||
<h1 style="margin: 0; color: #ffffff; font-size: 32px; font-weight: bold; letter-spacing: 0.5px;">BlindMaster</h1>
|
||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 14px; opacity: 0.95;">Smart Home Automation</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Status Icon -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 50px 40px 30px 40px;">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background-color: ${iconBg}; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 30px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
|
||||
<span style="color: #ffffff; font-size: 48px; font-weight: bold; line-height: 80px;">${icon}</span>
|
||||
</div>
|
||||
<h2 style="margin: 0 0 20px 0; color: #333333; font-size: 28px; font-weight: normal;">
|
||||
${title}
|
||||
</h2>
|
||||
<p style="margin: 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
||||
${message}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="border-top: 1px solid #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer info -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px; text-align: center;">
|
||||
<p style="margin: 0; color: #999999; font-size: 13px; line-height: 1.5;">
|
||||
You can safely close this window and return to the app.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer bar -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: #f9f9f9; padding: 25px 40px;">
|
||||
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||
© 2026 BlindMaster. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
sendEmailChangeVerification,
|
||||
generateVerificationPageHTML
|
||||
};
|
||||
|
||||
// Helper function to send email change verification email
|
||||
async function sendEmailChangeVerification(newEmail, token, name, oldEmail, localHour = new Date().getHours()) {
|
||||
const primaryColor = getColorForTime(localHour);
|
||||
const verificationLink = `https://wahwa.com/verify-email-change?token=${token}`;
|
||||
|
||||
const verificationLink = `https://blindmaster.wahwa.com/verify-email-change?token=${token}`;
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: `"BlindMaster" <${process.env.EMAIL_FROM}>`,
|
||||
const info = await sendMail({
|
||||
to: newEmail,
|
||||
subject: "Verify your new BlindMaster email address",
|
||||
html: `
|
||||
@@ -333,7 +256,7 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 600px;">
|
||||
|
||||
|
||||
<!-- Header with brand color -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: ${primaryColor}; padding: 40px 20px;">
|
||||
@@ -341,7 +264,7 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 14px; opacity: 0.95;">Smart Home Automation</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Message -->
|
||||
<tr>
|
||||
<td style="padding: 50px 40px 30px 40px; text-align: center;">
|
||||
@@ -359,24 +282,24 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- CTA Button -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 0 40px 40px 40px;">
|
||||
<a href="${verificationLink}"
|
||||
<a href="${verificationLink}"
|
||||
style="display: inline-block; padding: 16px 48px; background-color: ${primaryColor}; color: #ffffff; text-decoration: none; border-radius: 8px; font-size: 16px; font-weight: bold; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.3s ease;">
|
||||
Verify New Email
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="border-top: 1px solid #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer info -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px; text-align: center;">
|
||||
@@ -388,7 +311,7 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Footer bar -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: #f9f9f9; padding: 25px 40px;">
|
||||
@@ -397,7 +320,7 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -406,10 +329,95 @@ async function sendEmailChangeVerification(newEmail, token, name, oldEmail, loca
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
console.log("Email change verification sent successfully:", info.messageId);
|
||||
console.log("Email change verification sent:", info?.id || '(skipped)');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error sending email change verification:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to generate styled HTML response for verification pages
|
||||
function generateVerificationPageHTML(title, message, isSuccess = true) {
|
||||
const primaryColor = '#2196F3'; // Blue theme
|
||||
const icon = isSuccess ? '✓' : '✕';
|
||||
const iconBg = isSuccess ? primaryColor : '#f44336';
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css2?family=ABeeZee:ital@0;1&display=swap" rel="stylesheet">
|
||||
<title>${title} - BlindMaster</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f5f5f5; font-family: 'ABeeZee', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 40px 20px; min-height: 100vh;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 600px;">
|
||||
|
||||
<!-- Header with brand color -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: ${primaryColor}; padding: 40px 20px;">
|
||||
<h1 style="margin: 0; color: #ffffff; font-size: 32px; font-weight: bold; letter-spacing: 0.5px;">BlindMaster</h1>
|
||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 14px; opacity: 0.95;">Smart Home Automation</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Status Icon -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 50px 40px 30px 40px;">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background-color: ${iconBg}; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 30px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
|
||||
<span style="color: #ffffff; font-size: 48px; font-weight: bold; line-height: 80px;">${icon}</span>
|
||||
</div>
|
||||
<h2 style="margin: 0 0 20px 0; color: #333333; font-size: 28px; font-weight: normal;">
|
||||
${title}
|
||||
</h2>
|
||||
<p style="margin: 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
||||
${message}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="border-top: 1px solid #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer info -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px; text-align: center;">
|
||||
<p style="margin: 0; color: #999999; font-size: 13px; line-height: 1.5;">
|
||||
You can safely close this window and return to the app.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer bar -->
|
||||
<tr>
|
||||
<td align="center" style="background-color: #f9f9f9; padding: 25px 40px;">
|
||||
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||
© 2026 BlindMaster. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
sendEmailChangeVerification,
|
||||
generateVerificationPageHTML,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user