Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
# MONGODB_URI=mongodb://localhost:27017/iiitl-alumni

# MongoDB Atlas (production example)
# MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/iiitl
# MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/iiitl

# Mailgun Configuration
MAILGUN_API_KEY=""
MAILGUN_DOMAIN=""
# MAILGUN_URL="https://api.eu.mailgun.net" # Uncomment if using an EU region domain
EMAIL_FROM="no-reply@yourdomain.com"
1 change: 1 addition & 0 deletions .student-hub-ref
Submodule .student-hub-ref added at 3efe55
12 changes: 12 additions & 0 deletions lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI;

Check warning on line 3 in lib/db.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck-and-build

'MONGODB_URI' is assigned a value but never used

declare global {
var mongoose: {
Expand All @@ -16,7 +16,19 @@
cached = global.mongoose = { conn: null, promise: null };
}

/**
* Establishes a connection to the MongoDB database.
*
* This function utilizes connection caching to prevent multiple database connections
* during hot-reloading in development environments. It reads the `MONGODB_URI`
* environment variable dynamically at invocation to ensure it captures runtime environment configurations.
*
* @returns {Promise<typeof mongoose>} A promise that resolves to the Mongoose connection instance.
* @throws {Error} Throws an error if the `MONGODB_URI` environment variable is not defined.
*/
export async function connectDB() {
const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {
throw new Error("Please define the MONGODB_URI environment variable");
}
Expand Down
128 changes: 128 additions & 0 deletions lib/email/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import FormData from 'form-data';
import Mailgun from 'mailgun.js';
import EmailLog from '../../models/EmailLog';
import { connectDB } from '../db';

/**
* Options required to send an email via Mailgun.
*/
interface EmailOptions {
/** The recipient's email address. */
to: string;
/** The subject line of the email. */
subject: string;
/** The plain text body of the email. */
text: string;
/** The HTML encoded body of the email (optional, defaults to plain text if not provided). */
html?: string;
}

/**
* Validates the presence of required Mailgun environment variables.
*
* @returns {boolean} True if all required environment variables are present, otherwise false.
*/
function validateMailgunEnvVars(): boolean {
const requiredVars = ['MAILGUN_API_KEY', 'MAILGUN_DOMAIN'];
const missingVars = requiredVars.filter((varName) => !process.env[varName]);

if (missingVars.length > 0) {
console.warn(`Missing Mailgun environment variables: ${missingVars.join(', ')}`);
return false;
}
return true;
}

let mailgunClient: ReturnType<Mailgun['client']> | null = null;

/**
* Initializes and returns a singleton instance of the Mailgun client.
*
* The client is configured using the `MAILGUN_API_KEY` and `MAILGUN_URL`
* environment variables. It caches the instance to avoid re-initialization on subsequent calls.
*
* @returns {ReturnType<Mailgun['client']> | null} The authenticated Mailgun client instance, or null if initialization fails.
*/
function getMailgunClient() {
if (!mailgunClient && validateMailgunEnvVars()) {
const mailgun = new Mailgun(FormData);
mailgunClient = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY || '',
url: process.env.MAILGUN_URL || 'https://api.mailgun.net', // allows EU domains
});
}
return mailgunClient;
}

/**
* Sends an email using the Mailgun API and logs the attempt to the database.
*
* This function ensures the Mailgun client is properly configured and constructs
* the 'From' address using environment variables. It logs both successful sends
* and failed attempts to the `EmailLog` database collection.
*
* @param {EmailOptions} options - The configuration object for the email to be sent.
* @returns {Promise<boolean>} A promise that resolves to `true` if the email was successfully sent, or `false` if it failed.
*/
export async function sendEmail({ to, subject, text, html }: EmailOptions): Promise<boolean> {
const mg = getMailgunClient();

if (!mg || !process.env.MAILGUN_DOMAIN) {
console.error('Mailgun client not initialized or domain missing');
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Ensure from address is in proper format
const fromAddress = process.env.EMAIL_FROM || `no-reply@${process.env.MAILGUN_DOMAIN}`;

try {
const data = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
from: fromAddress,
to: [to],
subject,
text,
html: html || text,
});

console.log('Email sent successfully via Mailgun:', data.id);
await logEmail(to, subject, 'sent', data.id);
return true;
} catch (error: unknown) {
const err = error as { message?: string };
console.error('Mailgun error details:', err?.message);
await logEmail(to, subject, 'failed', undefined, err?.message);
return false;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

/**
* Logs an email delivery attempt to the MongoDB database.
*
* @param {string} to - The recipient's email address.
* @param {string} subject - The subject line of the email.
* @param {'sent' | 'failed'} status - The delivery outcome status.
* @param {string} [messageId] - The Mailgun message ID (applicable if successful).
* @param {string} [errorDetails] - The error message details (applicable if failed).
* @returns {Promise<void>}
*/
async function logEmail(
to: string,
subject: string,
status: 'sent' | 'failed',
messageId?: string,
errorDetails?: string
) {
try {
await connectDB();
await EmailLog.create({
to,
subject,
status,
messageId,
errorDetails,
});
} catch (dbError) {
console.error('Failed to log email to database:', dbError);
}
}
51 changes: 51 additions & 0 deletions models/EmailLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import mongoose, { Schema, Document, Model } from 'mongoose';

/**
* Represents a logged email delivery attempt in the database.
*/
export interface IEmailLog extends Document {
/** The recipient's email address. */
to: string;
/** The subject line of the email. */
subject: string;
/** The delivery status of the email. */
status: 'sent' | 'failed';
/** The unique message ID returned by Mailgun upon successful send (optional). */
messageId?: string;
/** Detailed error message if the email attempt failed (optional). */
errorDetails?: string;
/** Automatically generated timestamp of when the log was created. */
createdAt: Date;
/** Automatically generated timestamp of when the log was last updated. */
updatedAt: Date;
}

const EmailLogSchema: Schema<IEmailLog> = new Schema<IEmailLog>(
{
to: {
type: String,
required: [true, 'Recipient email is required'],
},
subject: {
type: String,
required: [true, 'Email subject is required'],
},
status: {
type: String,
enum: ['sent', 'failed'],
required: [true, 'Email status is required'],
},
messageId: {
type: String,
},
errorDetails: {
type: String,
},
},
{ timestamps: true }
);
Comment thread
DistantMyth marked this conversation as resolved.

const EmailLog: Model<IEmailLog> =
mongoose.models.EmailLog || mongoose.model<IEmailLog>('EmailLog', EmailLogSchema);

export default EmailLog;
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
},
"dependencies": {
"dotenv": "^17.4.1",
"form-data": "^4.0.5",
"mailgun.js": "^12.7.1",
"mongoose": "^9.4.1",
"next": "16.2.3",
"react": "19.2.4",
Expand All @@ -24,6 +26,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.3",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
}
Loading
Loading