feat: implement email sending functionality with SMTP and Resend fallback
Some checks failed
CI / skip-ci-check (pull_request) Successful in 29s
CI / lint-and-type-check (pull_request) Has been cancelled
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
Some checks failed
CI / skip-ci-check (pull_request) Successful in 29s
CI / lint-and-type-check (pull_request) Has been cancelled
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
- Added support for sending emails using SMTP configuration or Resend API as a fallback. - Updated environment variables in .env_example for SMTP settings. - Enhanced email confirmation process with improved error handling and fallback logic. - Introduced a test script to validate email sending configuration and functionality.
This commit is contained in:
parent
064daf47f7
commit
a5838a8373
@ -14,6 +14,25 @@ DATABASE_URL_WRITE="postgresql://viewer_write:password@localhost:5432/punimtag"
|
||||
NEXTAUTH_SECRET="your-secret-key-here-generate-with-openssl-rand-base64-32"
|
||||
NEXTAUTH_URL="http://localhost:3001"
|
||||
|
||||
# Email provider (smtp or resend)
|
||||
EMAIL_PROVIDER="smtp"
|
||||
|
||||
# SMTP (primary)
|
||||
SMTP_HOST="mail.your-domain.com"
|
||||
SMTP_PORT="465"
|
||||
SMTP_SECURE="true"
|
||||
SMTP_USER="mailer@your-domain.com"
|
||||
SMTP_PASS="your-mailbox-password"
|
||||
SMTP_FROM_EMAIL="noreply@your-domain.com"
|
||||
SMTP_FROM_NAME="PunimTag Viewer"
|
||||
SMTP_REPLY_TO="support@your-domain.com"
|
||||
|
||||
# Resend (fallback)
|
||||
RESEND_API_KEY="re_xxx"
|
||||
RESEND_FROM_EMAIL="onboarding@resend.dev"
|
||||
RESEND_FROM_NAME="PunimTag Viewer"
|
||||
RESEND_REPLY_TO="support@your-domain.com"
|
||||
|
||||
# Site Configuration
|
||||
NEXT_PUBLIC_SITE_NAME="PunimTag Photo Viewer"
|
||||
NEXT_PUBLIC_SITE_DESCRIPTION="Family Photo Gallery"
|
||||
|
||||
@ -10,6 +10,22 @@ NEXTAUTH_URL=http://127.0.0.1:3001
|
||||
NEXTAUTH_SECRET=CHANGE_ME_TO_A_LONG_RANDOM_STRING
|
||||
AUTH_URL=http://127.0.0.1:3001
|
||||
|
||||
# Email provider (smtp or resend)
|
||||
EMAIL_PROVIDER=smtp
|
||||
|
||||
# SMTP (primary)
|
||||
SMTP_HOST=mail.your-domain.com
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=mailer@your-domain.com
|
||||
SMTP_PASS=CHANGE_ME
|
||||
SMTP_FROM_EMAIL=noreply@your-domain.com
|
||||
SMTP_FROM_NAME=PunimTag
|
||||
SMTP_REPLY_TO=support@your-domain.com
|
||||
|
||||
# Resend (fallback)
|
||||
RESEND_API_KEY=CHANGE_ME_secret-key
|
||||
RESEND_FROM_EMAIL="onboarding@resend.dev"
|
||||
RESEND_FROM_NAME=PunimTag
|
||||
RESEND_REPLY_TO=support@your-domain.com
|
||||
UPLOAD_DIR="/mnt/db-server-uploads/pending-photos"
|
||||
|
||||
@ -1,8 +1,117 @@
|
||||
import { Resend } from 'resend';
|
||||
import crypto from 'crypto';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
type EmailPayload = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
function getBaseUrl(): string {
|
||||
return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
function formatFromAddress(fromEmail: string, fromName?: string): string {
|
||||
if (!fromName) {
|
||||
return fromEmail;
|
||||
}
|
||||
return `${fromName} <${fromEmail}>`;
|
||||
}
|
||||
|
||||
function getSmtpPort(): number {
|
||||
const port = Number(process.env.SMTP_PORT || '465');
|
||||
return Number.isNaN(port) ? 465 : port;
|
||||
}
|
||||
|
||||
function getSmtpSecure(): boolean {
|
||||
return (process.env.SMTP_SECURE || 'true').toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function hasSmtpConfig(): boolean {
|
||||
return Boolean(
|
||||
process.env.SMTP_HOST &&
|
||||
process.env.SMTP_USER &&
|
||||
process.env.SMTP_PASS
|
||||
);
|
||||
}
|
||||
|
||||
async function sendViaSmtp(payload: EmailPayload): Promise<void> {
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
throw new Error('Missing SMTP configuration (SMTP_HOST, SMTP_USER, SMTP_PASS)');
|
||||
}
|
||||
|
||||
const smtpFromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||
const smtpFromName = process.env.SMTP_FROM_NAME;
|
||||
const smtpReplyTo = process.env.SMTP_REPLY_TO || smtpFromEmail;
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: getSmtpPort(),
|
||||
secure: getSmtpSecure(),
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: formatFromAddress(smtpFromEmail, smtpFromName),
|
||||
to: payload.to,
|
||||
replyTo: smtpReplyTo,
|
||||
subject: payload.subject,
|
||||
text: payload.text,
|
||||
html: payload.html,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendViaResend(payload: EmailPayload): Promise<void> {
|
||||
const fromEmail = process.env.RESEND_FROM_EMAIL || process.env.SMTP_FROM_EMAIL || 'onboarding@resend.dev';
|
||||
const fromName = process.env.RESEND_FROM_NAME || process.env.SMTP_FROM_NAME;
|
||||
const replyTo = process.env.RESEND_REPLY_TO || process.env.SMTP_REPLY_TO || fromEmail;
|
||||
|
||||
const result = await resend.emails.send({
|
||||
from: formatFromAddress(fromEmail, fromName),
|
||||
to: payload.to,
|
||||
replyTo,
|
||||
subject: payload.subject,
|
||||
text: payload.text,
|
||||
html: payload.html,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`Resend API error: ${result.error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEmailWithFallback(payload: EmailPayload): Promise<void> {
|
||||
const provider = (process.env.EMAIL_PROVIDER || 'smtp').toLowerCase();
|
||||
|
||||
if (provider == 'resend') {
|
||||
await sendViaResend(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasSmtpConfig()) {
|
||||
await sendViaResend(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendViaSmtp(payload);
|
||||
} catch (smtpError) {
|
||||
console.error('[EMAIL] SMTP send failed, trying Resend fallback:', smtpError);
|
||||
await sendViaResend(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateEmailConfirmationToken(): string {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
@ -12,44 +121,47 @@ export async function sendEmailConfirmation(
|
||||
name: string,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001';
|
||||
const confirmationUrl = `${baseUrl}/api/auth/verify-email?token=${token}`;
|
||||
const confirmationUrl = `${getBaseUrl()}/api/auth/verify-email?token=${token}`;
|
||||
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL || 'onboarding@resend.dev',
|
||||
to: email,
|
||||
subject: 'Confirm your email address',
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirm your email</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
|
||||
<h1 style="color: #2563eb; margin-top: 0;">Confirm your email address</h1>
|
||||
<p>Hi ${name},</p>
|
||||
<p>Thank you for signing up! Please confirm your email address by clicking the button below:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Confirm Email Address</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px;">${confirmationUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
If you didn't create an account, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending confirmation email:', error);
|
||||
throw new Error('Failed to send confirmation email');
|
||||
}
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirm your email</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
|
||||
<h1 style="color: #2563eb; margin-top: 0;">Confirm your email address</h1>
|
||||
<p>Hi ${name},</p>
|
||||
<p>Thank you for signing up! Please confirm your email address by clicking the button below:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Confirm Email Address</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px;">${confirmationUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
If you didn't create an account, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const text = `Hi ${name},
|
||||
|
||||
Thank you for signing up. Please confirm your email address:
|
||||
${confirmationUrl}
|
||||
|
||||
If you did not create this account, you can safely ignore this email.`;
|
||||
|
||||
await sendEmailWithFallback({
|
||||
to: email,
|
||||
subject: 'Confirm your email address',
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendEmailConfirmationResend(
|
||||
@ -57,44 +169,47 @@ export async function sendEmailConfirmationResend(
|
||||
name: string,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001';
|
||||
const confirmationUrl = `${baseUrl}/api/auth/verify-email?token=${token}`;
|
||||
const confirmationUrl = `${getBaseUrl()}/api/auth/verify-email?token=${token}`;
|
||||
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL || 'onboarding@resend.dev',
|
||||
to: email,
|
||||
subject: 'Confirm your email address',
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirm your email</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
|
||||
<h1 style="color: #2563eb; margin-top: 0;">Confirm your email address</h1>
|
||||
<p>Hi ${name},</p>
|
||||
<p>You requested a new confirmation email. Please confirm your email address by clicking the button below:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Confirm Email Address</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px;">${confirmationUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
This link will expire in 24 hours. If you didn't request this email, you can safely ignore it.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending confirmation email:', error);
|
||||
throw new Error('Failed to send confirmation email');
|
||||
}
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirm your email</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
|
||||
<h1 style="color: #2563eb; margin-top: 0;">Confirm your email address</h1>
|
||||
<p>Hi ${name},</p>
|
||||
<p>You requested a new confirmation email. Please confirm your email address by clicking the button below:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold;">Confirm Email Address</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px;">${confirmationUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
This link will expire in 24 hours. If you didn't request this email, you can safely ignore it.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const text = `Hi ${name},
|
||||
|
||||
You requested a new confirmation email. Please confirm your email address:
|
||||
${confirmationUrl}
|
||||
|
||||
This link expires in 24 hours. If you did not request this, you can ignore this email.`;
|
||||
|
||||
await sendEmailWithFallback({
|
||||
to: email,
|
||||
subject: 'Confirm your email address',
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
export function generatePasswordResetToken(): string {
|
||||
@ -106,12 +221,8 @@ export async function sendPasswordResetEmail(
|
||||
name: string,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001';
|
||||
const resetUrl = `${baseUrl}/reset-password?token=${token}`;
|
||||
const fromEmail = process.env.RESEND_FROM_EMAIL || 'onboarding@resend.dev';
|
||||
const replyTo = process.env.RESEND_REPLY_TO || fromEmail;
|
||||
const resetUrl = `${getBaseUrl()}/reset-password?token=${token}`;
|
||||
|
||||
// Plain text version for better deliverability
|
||||
const text = `Hi ${name},
|
||||
|
||||
You requested to reset your password. Click the link below to create a new password:
|
||||
@ -125,72 +236,43 @@ If you didn't request a password reset, you can safely ignore this email.
|
||||
Best regards,
|
||||
PunimTag Viewer Team`;
|
||||
|
||||
try {
|
||||
console.log('[EMAIL] Sending password reset email:', {
|
||||
from: fromEmail,
|
||||
to: email,
|
||||
replyTo: replyTo,
|
||||
});
|
||||
|
||||
const result = await resend.emails.send({
|
||||
from: fromEmail,
|
||||
to: email,
|
||||
replyTo: replyTo,
|
||||
subject: 'Reset your password - PunimTag Viewer',
|
||||
text: text,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Reset your password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #ffffff;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
||||
<h1 style="color: #2563eb; margin-top: 0; font-size: 24px;">Reset your password</h1>
|
||||
<p style="font-size: 16px;">Hi ${name},</p>
|
||||
<p style="font-size: 16px;">You requested to reset your password for your PunimTag Viewer account. Click the button below to create a new password:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${resetUrl}" style="background-color: #2563eb; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold; font-size: 16px;">Reset Password</a>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #666;">Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px; background-color: #ffffff; padding: 10px; border-radius: 4px; border: 1px solid #e5e7eb;">${resetUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
<strong>Important:</strong> This link will expire in 1 hour for security reasons.
|
||||
</p>
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
||||
</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 12px;">
|
||||
This is an automated message from PunimTag Viewer. Please do not reply to this email.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error('[EMAIL] Resend API error:', result.error);
|
||||
throw new Error(`Resend API error: ${result.error.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
console.log('[EMAIL] Password reset email sent successfully:', {
|
||||
emailId: result.data?.id,
|
||||
to: email,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[EMAIL] Error sending password reset email:', error);
|
||||
console.error('[EMAIL] Error details:', {
|
||||
message: error?.message,
|
||||
name: error?.name,
|
||||
response: error?.response,
|
||||
statusCode: error?.statusCode,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Reset your password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #ffffff;">
|
||||
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
||||
<h1 style="color: #2563eb; margin-top: 0; font-size: 24px;">Reset your password</h1>
|
||||
<p style="font-size: 16px;">Hi ${name},</p>
|
||||
<p style="font-size: 16px;">You requested to reset your password for your PunimTag Viewer account. Click the button below to create a new password:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${resetUrl}" style="background-color: #2563eb; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; font-weight: bold; font-size: 16px;">Reset Password</a>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #666;">Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666; font-size: 14px; background-color: #ffffff; padding: 10px; border-radius: 4px; border: 1px solid #e5e7eb;">${resetUrl}</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||
<strong>Important:</strong> This link will expire in 1 hour for security reasons.
|
||||
</p>
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
||||
</p>
|
||||
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 12px;">
|
||||
This is an automated message from PunimTag Viewer. Please do not reply to this email.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await sendEmailWithFallback({
|
||||
to: email,
|
||||
subject: 'Reset your password - PunimTag Viewer',
|
||||
text,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
21
viewer-frontend/package-lock.json
generated
21
viewer-frontend/package-lock.json
generated
@ -27,6 +27,7 @@
|
||||
"lucide-react": "^0.553.0",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"nodemailer": "^7.0.13",
|
||||
"prisma": "^6.19.0",
|
||||
"react": "19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
@ -40,6 +41,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.2.3",
|
||||
@ -2021,6 +2023,16 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
|
||||
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.7",
|
||||
"devOptional": true,
|
||||
@ -6116,6 +6128,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"lucide-react": "^0.553.0",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"nodemailer": "^7.0.13",
|
||||
"prisma": "^6.19.0",
|
||||
"react": "19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
@ -51,6 +52,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.2.3",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Resend } from 'resend';
|
||||
import nodemailer from 'nodemailer';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
@ -6,63 +7,96 @@ dotenv.config();
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
function smtpEnabled(): boolean {
|
||||
return Boolean(
|
||||
process.env.SMTP_HOST &&
|
||||
process.env.SMTP_USER &&
|
||||
process.env.SMTP_PASS
|
||||
);
|
||||
}
|
||||
|
||||
async function testEmailSending() {
|
||||
console.log('🧪 Testing email sending configuration...\n');
|
||||
|
||||
// Check environment variables
|
||||
console.log('📋 Environment Variables:');
|
||||
console.log(' EMAIL_PROVIDER:', process.env.EMAIL_PROVIDER || 'smtp (default)');
|
||||
console.log(' SMTP_HOST:', process.env.SMTP_HOST || '❌ NOT SET');
|
||||
console.log(' SMTP_PORT:', process.env.SMTP_PORT || '465 (default)');
|
||||
console.log(' SMTP_SECURE:', process.env.SMTP_SECURE || 'true (default)');
|
||||
console.log(' SMTP_USER:', process.env.SMTP_USER || '❌ NOT SET');
|
||||
console.log(' SMTP_FROM_EMAIL:', process.env.SMTP_FROM_EMAIL || '❌ NOT SET');
|
||||
console.log(' RESEND_API_KEY:', process.env.RESEND_API_KEY ? `${process.env.RESEND_API_KEY.substring(0, 10)}...` : '❌ NOT SET');
|
||||
console.log(' RESEND_FROM_EMAIL:', process.env.RESEND_FROM_EMAIL || '❌ NOT SET');
|
||||
console.log(' NEXTAUTH_URL:', process.env.NEXTAUTH_URL || '❌ NOT SET');
|
||||
console.log('');
|
||||
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
console.error('❌ RESEND_API_KEY is not set in .env file');
|
||||
|
||||
const toEmail = process.env.TEST_EMAIL_TO || process.env.SMTP_USER || process.env.RESEND_FROM_EMAIL;
|
||||
if (!toEmail) {
|
||||
console.error('❌ Set TEST_EMAIL_TO or SMTP_USER in .env to run this test.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.RESEND_FROM_EMAIL) {
|
||||
console.error('❌ RESEND_FROM_EMAIL is not set in .env file');
|
||||
|
||||
if (smtpEnabled()) {
|
||||
console.log('📤 Attempting SMTP test first...');
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT || 465),
|
||||
secure: (process.env.SMTP_SECURE || 'true').toLowerCase() === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const smtpFromEmail = process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER!;
|
||||
const smtpFromName = process.env.SMTP_FROM_NAME;
|
||||
const smtpReplyTo = process.env.SMTP_REPLY_TO || smtpFromEmail;
|
||||
const smtpFrom = smtpFromName
|
||||
? `${smtpFromName} <${smtpFromEmail}>`
|
||||
: smtpFromEmail;
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: smtpFrom,
|
||||
to: toEmail,
|
||||
replyTo: smtpReplyTo,
|
||||
subject: 'SMTP test from PunimTag',
|
||||
text: 'SMTP test email from PunimTag viewer frontend.',
|
||||
});
|
||||
|
||||
console.log('✅ SMTP test sent successfully');
|
||||
console.log(' Message ID:', info.messageId);
|
||||
process.exit(0);
|
||||
} catch (smtpError: any) {
|
||||
console.error('⚠️ SMTP test failed, will try Resend fallback.');
|
||||
console.error(' SMTP error:', smtpError.message);
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ SMTP config not set, skipping SMTP test.');
|
||||
}
|
||||
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_FROM_EMAIL) {
|
||||
console.error('❌ Resend fallback not configured (RESEND_API_KEY/RESEND_FROM_EMAIL missing).');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clean up the from email (remove quotes and spaces)
|
||||
const fromEmail = process.env.RESEND_FROM_EMAIL.trim().replace(/^["']|["']$/g, '');
|
||||
console.log('📧 Using FROM email:', fromEmail);
|
||||
console.log('');
|
||||
|
||||
// Test email sending
|
||||
console.log('📤 Attempting to send test email...');
|
||||
|
||||
const resendFrom = process.env.RESEND_FROM_EMAIL.trim().replace(/^["']|["']$/g, '');
|
||||
console.log('📤 Attempting Resend fallback test...');
|
||||
try {
|
||||
const result = await resend.emails.send({
|
||||
from: fromEmail,
|
||||
to: 'test@example.com', // This will fail but we'll see the error
|
||||
subject: 'Test Email',
|
||||
html: '<p>This is a test email</p>',
|
||||
from: resendFrom,
|
||||
to: toEmail,
|
||||
subject: 'Resend fallback test from PunimTag',
|
||||
html: '<p>Resend fallback test email from PunimTag viewer frontend.</p>',
|
||||
});
|
||||
|
||||
console.log('✅ Email API call successful!');
|
||||
console.log('Response:', JSON.stringify(result, null, 2));
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message || 'Unknown Resend API error');
|
||||
}
|
||||
console.log('✅ Resend fallback test sent successfully');
|
||||
console.log(' Email ID:', result.data?.id);
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error sending email:');
|
||||
console.error(' Message:', error.message);
|
||||
if (error.response) {
|
||||
console.error(' Response:', JSON.stringify(error.response, null, 2));
|
||||
}
|
||||
|
||||
// Check for common errors
|
||||
if (error.message?.includes('domain')) {
|
||||
console.error('\n⚠️ Domain verification issue:');
|
||||
console.error(' The email domain needs to be verified in Resend dashboard');
|
||||
console.error(' For testing, use: onboarding@resend.dev');
|
||||
}
|
||||
|
||||
if (error.message?.includes('unauthorized') || error.message?.includes('Invalid API key')) {
|
||||
console.error('\n⚠️ API Key issue:');
|
||||
console.error(' Check that your RESEND_API_KEY is correct');
|
||||
console.error(' Get a new key from: https://resend.com/api-keys');
|
||||
}
|
||||
|
||||
console.error('❌ Resend fallback failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user