diff --git a/app-next/src/lib/email-templates/admin-upload.ts b/app-next/src/lib/email-templates/admin-upload.ts
new file mode 100644
index 00000000..e07e55e3
--- /dev/null
+++ b/app-next/src/lib/email-templates/admin-upload.ts
@@ -0,0 +1,46 @@
+// Email specific components
+import { baseLayout } from "./layout";
+
+export const generateAdminReviewEmail = (
+ userName: string,
+ datasetId: string,
+ datasetName: string,
+ logoUrl: string,
+ datasetUrl?: string,
+) => {
+ const datasetLink = datasetUrl ?? `https://www.openml.org/datasets/${datasetId}`;
+
+ const content = `
+
+
New Dataset Uploaded
+
+
+
Upload Details
+
+
+ | User: |
+ ${userName} |
+
+
+ | Dataset: |
+ ${datasetName} (ID: ${datasetId}) |
+
+
+ | Time: |
+ ${new Date().toLocaleString()} |
+
+
+
+
+
+
+
+
+ This is an automated notification for OpenML Admins.
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/email-templates/confirm-email.ts b/app-next/src/lib/email-templates/confirm-email.ts
new file mode 100644
index 00000000..9e43235e
--- /dev/null
+++ b/app-next/src/lib/email-templates/confirm-email.ts
@@ -0,0 +1,33 @@
+// Email specific components
+import { baseLayout } from "./layout";
+
+export const generateConfirmEmail = (link: string, logoUrl: string) => {
+ const content = `
+
+ Welcome to OpenML!
+
+ Hi there,
+ Thank you for signing up to OpenML, the collaborative machine learning platform.
+ To verify your email address and activate your account, please click the button below:
+
+
+
+
+
+
+
Or copy and paste this link into your browser:
+
${link}
+
+
+
+ ⏱️ This confirmation link will expire in 24 hours.
+
+
+ If you didn't create an account with OpenML, you can safely ignore this email.
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/email-templates/dataset-edit.ts b/app-next/src/lib/email-templates/dataset-edit.ts
new file mode 100644
index 00000000..6da6db8c
--- /dev/null
+++ b/app-next/src/lib/email-templates/dataset-edit.ts
@@ -0,0 +1,48 @@
+// Email specific components
+import { baseLayout } from "./layout";
+
+export const generateDatasetEditEmail = (
+ userName: string,
+ datasetId: string,
+ datasetName: string,
+ changes: string[],
+ logoUrl: string,
+ datasetUrl?: string,
+) => {
+ const datasetLink = datasetUrl ?? `https://www.openml.org/datasets/${datasetId}`;
+
+ const content = `
+
+ Dataset Updated
+
+
+
Dataset Details
+
+
+ | User: |
+ ${userName} |
+
+
+ | Dataset: |
+ ${datasetName} (ID: ${datasetId}) |
+
+
+
+
Changes Made:
+
+ ${changes.map((change) => `- ${change}
`).join("")}
+
+
+
+
+
+
+
+ This is an automated notification for OpenML Admins/Owners.
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/email-templates/entity-created.ts b/app-next/src/lib/email-templates/entity-created.ts
new file mode 100644
index 00000000..00cfa4be
--- /dev/null
+++ b/app-next/src/lib/email-templates/entity-created.ts
@@ -0,0 +1,51 @@
+import { baseLayout } from "./layout";
+
+type EntityType = "dataset" | "task" | "collection";
+
+const ENTITY_LABELS: Record = {
+ dataset: "Dataset",
+ task: "Task",
+ collection: "Collection",
+};
+
+export const generateEntityCreatedEmail = (
+ entityType: EntityType,
+ entityName: string,
+ entityId: string,
+ entityUrl: string,
+ logoUrl: string,
+) => {
+ const label = ENTITY_LABELS[entityType];
+
+ const content = `
+ ${label} Created Successfully
+
+
+
${label} Details
+
+
+ | ${label}: |
+ ${entityName} |
+
+
+ | ID: |
+ ${entityId} |
+
+
+
+
+ Your ${label.toLowerCase()} has been submitted to OpenML and will be available shortly.
+
+
+
+
+
+
+ This is an automated confirmation from OpenML.
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/email-templates/layout.ts b/app-next/src/lib/email-templates/layout.ts
new file mode 100644
index 00000000..fdcfa3a0
--- /dev/null
+++ b/app-next/src/lib/email-templates/layout.ts
@@ -0,0 +1,64 @@
+export const baseLayout = (content: string, logoUrl: string) => `
+
+
+
+
+
+ OpenML Email
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.
+
+
+
+
+
+`;
diff --git a/app-next/src/lib/email-templates/profile-update.ts b/app-next/src/lib/email-templates/profile-update.ts
new file mode 100644
index 00000000..fb6be680
--- /dev/null
+++ b/app-next/src/lib/email-templates/profile-update.ts
@@ -0,0 +1,37 @@
+// Email specific components
+import { baseLayout } from "./layout";
+
+export const generateProfileUpdateEmail = (
+ name: string,
+ fields: string[],
+ logoUrl: string,
+) => {
+ const content = `
+
+ Security Alert: Profile Updated
+
+ Hi ${name},
+
+ This email is to notify you that your OpenML profile was recently updated.
+
+
+
+
+
The following items were changed:
+
+ ${fields.map((field) => `- ${field}
`).join("")}
+
+
+
+
+ If you made these changes, you can safely ignore this email.
+
+
+
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/email-templates/reset-password.ts b/app-next/src/lib/email-templates/reset-password.ts
new file mode 100644
index 00000000..ba501fa9
--- /dev/null
+++ b/app-next/src/lib/email-templates/reset-password.ts
@@ -0,0 +1,35 @@
+import { baseLayout } from "./layout";
+
+export const generateResetPasswordEmail = (link: string, logoUrl: string) => {
+ const content = `
+
+ Reset Your Password
+
+ Hi there,
+ We received a request to reset your password for your OpenML account.
+ Click the button below to create a new password:
+
+
+
+
+
+
+
+
Or copy and paste this link into your browser:
+
${link}
+
+
+
+ ⏱️ This reset link will expire in 1 hour.
+
+
+
+
+
🔒 Security Notice: If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
+
+ `;
+
+ return baseLayout(content, logoUrl);
+};
diff --git a/app-next/src/lib/mail.ts b/app-next/src/lib/mail.ts
index f5d9619b..fd321929 100644
--- a/app-next/src/lib/mail.ts
+++ b/app-next/src/lib/mail.ts
@@ -1,9 +1,13 @@
import nodemailer from "nodemailer";
+import { APP_CONFIG } from "@/lib/config";
+import { generateConfirmEmail } from "./email-templates/confirm-email";
+import { generateResetPasswordEmail } from "./email-templates/reset-password";
+import { generateProfileUpdateEmail } from "./email-templates/profile-update";
+import { generateAdminReviewEmail } from "./email-templates/admin-upload";
+import { generateDatasetEditEmail } from "./email-templates/dataset-edit";
+import { generateEntityCreatedEmail } from "./email-templates/entity-created";
-/**
- * Shared email utility for sending system emails
- */
-
+// Shared email utility for sending system emails
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || "localhost",
port: parseInt(process.env.SMTP_PORT || "1025"),
@@ -17,60 +21,19 @@ const transporter = nodemailer.createTransport({
: undefined,
});
-/**
- * Send account confirmation email
- */
+// Send account confirmation email
export async function sendConfirmationEmail(email: string, token: string) {
- const baseUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3050";
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050";
const link = `${baseUrl}/auth/confirm-email?token=${token}`;
const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+ const htmlContent = generateConfirmEmail(link, logoUrl);
+
const mailOptions = {
from: process.env.SMTP_FROM || '"OpenML" ',
to: email,
subject: "Welcome to OpenML!",
- html: `
-
-
-
-

-
Welcome to OpenML!
-
-
-
-
-
Hi there,
-
Thank you for signing up to OpenML, the collaborative machine learning platform.
-
To verify your email address and activate your account, please click the button below:
-
-
-
-
-
-
-
Or copy and paste this link into your browser:
-
${link}
-
-
-
⏱️ This confirmation link will expire in 24 hours.
-
If you didn't create an account with OpenML, you can safely ignore this email.
-
-
-
-
-
-
-
-
© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.
-
-
- `,
+ html: htmlContent,
text: `Hi,
Thank you for signing up to OpenML.
@@ -95,64 +58,19 @@ The OpenML team`,
}
}
-/**
- * Send password reset email
- */
+//Send password reset email
export async function sendPasswordResetEmail(email: string, token: string) {
- const baseUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3050";
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050";
const link = `${baseUrl}/auth/reset-password?token=${token}`;
const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+ const htmlContent = generateResetPasswordEmail(link, logoUrl);
+
const mailOptions = {
from: process.env.SMTP_FROM || '"OpenML" ',
to: email,
subject: "Reset Your OpenML Password",
- html: `
-
-
-
-

-
Reset Your Password
-
-
-
-
-
Hi there,
-
We received a request to reset your password for your OpenML account.
-
Click the button below to create a new password:
-
-
-
-
-
-
-
Or copy and paste this link into your browser:
-
${link}
-
-
-
⏱️ This reset link will expire in 1 hour.
-
-
-
-
🔒 Security Notice: If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
-
-
-
-
-
-
-
-
-
© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.
-
-
- `,
+ html: htmlContent,
text: `Hi,
We received a request to reset your password for your OpenML account.
@@ -179,3 +97,171 @@ The OpenML Team`,
return { success: false, error };
}
}
+
+// Send profile update notification
+export async function sendProfileUpdateEmail(email: string, name: string) {
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000";
+ const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+ const supportUrl = "mailto:openmachinelearning@gmail.com";
+
+ const htmlContent = generateProfileUpdateEmail(
+ name,
+ ["Profile Details"],
+ logoUrl,
+ );
+
+ const mailOptions = {
+ from: process.env.SMTP_FROM || '"OpenML" ',
+ to: email,
+ subject: "Security Alert: Profile Updated",
+ html: htmlContent,
+ text: `Hi ${name},
+
+Your OpenML profile has been updated.
+
+If you made this change, you can safely ignore this email.
+If you did not authorize this change, please contact us immediately at ${supportUrl}.
+
+Best regards,
+The OpenML Team`,
+ };
+
+ try {
+ const info = await transporter.sendMail(mailOptions);
+ console.log("Profile update email sent:", info.messageId);
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending profile update email:", error);
+ return { success: false, error };
+ }
+}
+
+//Send dataset upload notification to admins
+export async function sendDatasetUploadEmail(
+ uploaderName: string,
+ datasetName: string,
+ datasetId: string | number,
+) {
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000";
+ const datasetUrl = `${baseUrl}/datasets/${datasetId}`;
+ const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+
+ const htmlContent = generateAdminReviewEmail(
+ uploaderName,
+ datasetId.toString(),
+ datasetName,
+ logoUrl,
+ datasetUrl,
+ );
+
+ // Send to ADMIN_EMAIL or default support email
+ const adminEmail = process.env.ADMIN_EMAIL || "openmachinelearning@gmail.com";
+
+ const mailOptions = {
+ from: process.env.SMTP_FROM || '"OpenML" ',
+ to: adminEmail,
+ subject: `New Dataset Upload: ${datasetName}`,
+ html: htmlContent,
+ text: `New Dataset Upload
+
+User: ${uploaderName}
+Dataset: ${datasetName}
+Link: ${datasetUrl}
+
+Please review this dataset to ensure it meets our quality standards.`,
+ };
+
+ try {
+ const info = await transporter.sendMail(mailOptions);
+ console.log("Dataset upload email sent:", info.messageId);
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending dataset upload email:", error);
+ return { success: false, error };
+ }
+}
+
+// Send creation confirmation to the creator (dataset, task, or collection)
+export async function sendCreationConfirmationEmail(
+ recipientEmail: string,
+ entityType: "dataset" | "task" | "collection",
+ entityName: string,
+ entityId: string | number,
+) {
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050";
+ const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+ const pathMap = { dataset: "datasets", task: "tasks", collection: "collections" };
+ const entityUrl = `${baseUrl}/${pathMap[entityType]}/${entityId}`;
+
+ const htmlContent = generateEntityCreatedEmail(
+ entityType,
+ entityName,
+ entityId.toString(),
+ entityUrl,
+ logoUrl,
+ );
+
+ const labelMap = { dataset: "Dataset", task: "Task", collection: "Collection" };
+ const label = labelMap[entityType];
+
+ const mailOptions = {
+ from: process.env.SMTP_FROM || '"OpenML" ',
+ to: recipientEmail,
+ subject: `Your OpenML ${label} "${entityName}" has been created`,
+ html: htmlContent,
+ text: `Your ${label} "${entityName}" has been successfully submitted to OpenML.\nView it at: ${entityUrl}\n\nBest regards,\nThe OpenML Team`,
+ };
+
+ try {
+ const info = await transporter.sendMail(mailOptions);
+ console.log(`${label} creation email sent:`, info.messageId);
+ return { success: true };
+ } catch (error) {
+ console.error(`Error sending ${label} creation email:`, error);
+ return { success: false, error };
+ }
+}
+
+// Send dataset edit notification
+
+export async function sendDatasetEditEmail(
+ recipientEmail: string,
+ datasetName: string,
+ datasetId: string | number,
+) {
+ const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000";
+ const datasetUrl = `${baseUrl}/datasets/${datasetId}`;
+ const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`;
+
+ const htmlContent = generateDatasetEditEmail(
+ "User",
+ datasetId.toString(),
+ datasetName,
+ ["Metadata updated"], // changes
+ logoUrl,
+ datasetUrl,
+ );
+
+ const mailOptions = {
+ from: process.env.SMTP_FROM || '"OpenML" ',
+ to: recipientEmail,
+ subject: `Update: Dataset "${datasetName}" has been modified`,
+ html: htmlContent,
+ text: `Dataset Update
+
+The dataset "${datasetName}" has been updated.
+View the changes here: ${datasetUrl}
+
+Best regards,
+The OpenML Team`,
+ };
+
+ try {
+ const info = await transporter.sendMail(mailOptions);
+ console.log("Dataset edit email sent:", info.messageId);
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending dataset edit email:", error);
+ return { success: false, error };
+ }
+}