diff --git a/bun.lockb b/bun.lockb index d836440..4e361e7 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/dist/package-shared/actions/users/login-user.d.ts b/dist/package-shared/actions/users/login-user.d.ts index ead9866..df2bb75 100644 --- a/dist/package-shared/actions/users/login-user.d.ts +++ b/dist/package-shared/actions/users/login-user.d.ts @@ -9,6 +9,9 @@ type Param = { password?: string; }; additionalFields?: string[]; + request?: http.IncomingMessage & { + [s: string]: any; + }; response?: http.ServerResponse & { [s: string]: any; }; @@ -30,5 +33,5 @@ type Param = { /** * # Login A user */ -export default function loginUser({ key, payload, database, additionalFields, response, encryptionKey, encryptionSalt, email_login, email_login_code, temp_code_field, token, user_id, skipPassword, apiUserID, skipWriteAuthFile, dbUserId, debug, cleanupTokens, secureCookie, }: Param): Promise; +export default function loginUser({ key, payload, database, additionalFields, response, encryptionKey, encryptionSalt, email_login, email_login_code, temp_code_field, token, user_id, skipPassword, apiUserID, skipWriteAuthFile, dbUserId, debug, cleanupTokens, secureCookie, request, }: Param): Promise; export {}; diff --git a/dist/package-shared/actions/users/login-user.js b/dist/package-shared/actions/users/login-user.js index 5c9ce53..b7f2187 100644 --- a/dist/package-shared/actions/users/login-user.js +++ b/dist/package-shared/actions/users/login-user.js @@ -22,11 +22,12 @@ const get_auth_cookie_names_1 = __importDefault(require("../../functions/backend const write_auth_files_1 = require("../../functions/backend/auth/write-auth-files"); const debug_log_1 = __importDefault(require("../../utils/logging/debug-log")); const grab_cookie_expirt_date_1 = __importDefault(require("../../utils/grab-cookie-expirt-date")); +const validate_email_1 = __importDefault(require("../../functions/email/fns/validate-email")); /** * # Login A user */ function loginUser(_a) { - return __awaiter(this, arguments, void 0, function* ({ key, payload, database, additionalFields, response, encryptionKey, encryptionSalt, email_login, email_login_code, temp_code_field, token, user_id, skipPassword, apiUserID, skipWriteAuthFile, dbUserId, debug, cleanupTokens, secureCookie, }) { + return __awaiter(this, arguments, void 0, function* ({ key, payload, database, additionalFields, response, encryptionKey, encryptionSalt, email_login, email_login_code, temp_code_field, token, user_id, skipPassword, apiUserID, skipWriteAuthFile, dbUserId, debug, cleanupTokens, secureCookie, request, }) { var _b, _c, _d; const grabedHostNames = (0, grab_host_names_1.default)({ userId: user_id || apiUserID }); const { host, port, scheme } = grabedHostNames; @@ -63,11 +64,12 @@ function loginUser(_a) { * * @description Check required fields */ - if (!payload.email) { + const isEmailValid = yield (0, validate_email_1.default)({ email: payload.email }); + if (!payload.email || !isEmailValid.isValid) { return { success: false, payload: null, - msg: "Email Required", + msg: isEmailValid.message, }; } /** diff --git a/dist/package-shared/external-services/arcjet/index.d.ts b/dist/package-shared/external-services/arcjet/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/dist/package-shared/external-services/arcjet/index.js b/dist/package-shared/external-services/arcjet/index.js new file mode 100644 index 0000000..4b13db2 --- /dev/null +++ b/dist/package-shared/external-services/arcjet/index.js @@ -0,0 +1,27 @@ +"use strict"; +// import arcjet, { ArcjetOptions, Primitive, Product } from "@arcjet/node"; +// interface Params< +// Rules extends (Primitive | Product)[], +// Characteristics extends readonly string[] +// > { +// options?: Omit, "key" | "rules"> & { +// rules?: Rules; +// }; +// } +// export default function arcjetClient< +// Rules extends (Primitive | Product)[], +// Characteristics extends readonly string[] +// >(params?: Params) { +// const ARCJET_KEY = process.env.DSQL_ARCJET_KEY; +// const ARCJET_ENV = process.env.NODE_ENV || "development"; +// if (!ARCJET_KEY) { +// return null; +// } +// const aj = arcjet({ +// key: ARCJET_KEY, +// characteristics: ["ip.src"], +// rules: [], +// ...params?.options, +// }); +// return aj; +// } diff --git a/dist/package-shared/functions/api/users/api-create-user.d.ts b/dist/package-shared/functions/api/users/api-create-user.d.ts index e1d770b..85eed9f 100644 --- a/dist/package-shared/functions/api/users/api-create-user.d.ts +++ b/dist/package-shared/functions/api/users/api-create-user.d.ts @@ -5,12 +5,12 @@ import { APICreateUserFunctionParams } from "../../../types"; export default function apiCreateUser({ encryptionKey, payload, database, userId, }: APICreateUserFunctionParams): Promise<{ success: boolean; msg: string; - payload: null; + payload?: undefined; sqlResult?: undefined; } | { success: boolean; - msg: string; - payload?: undefined; + msg: string | undefined; + payload: null; sqlResult?: undefined; } | { success: boolean; diff --git a/dist/package-shared/functions/api/users/api-create-user.js b/dist/package-shared/functions/api/users/api-create-user.js index 1d89398..4d56ed7 100644 --- a/dist/package-shared/functions/api/users/api-create-user.js +++ b/dist/package-shared/functions/api/users/api-create-user.js @@ -19,6 +19,7 @@ const addDbEntry_1 = __importDefault(require("../../backend/db/addDbEntry")); const updateUsersTableSchema_1 = __importDefault(require("../../backend/updateUsersTableSchema")); const varDatabaseDbHandler_1 = __importDefault(require("../../backend/varDatabaseDbHandler")); const hashPassword_1 = __importDefault(require("../../dsql/hashPassword")); +const validate_email_1 = __importDefault(require("../../email/fns/validate-email")); /** * # API Create User */ @@ -104,6 +105,14 @@ function apiCreateUser(_a) { payload: null, }; } + const isEmailValid = yield (0, validate_email_1.default)({ email: payload.email }); + if (!isEmailValid.isValid) { + return { + success: false, + msg: isEmailValid.message, + payload: null, + }; + } const addUser = yield (0, addDbEntry_1.default)({ dbFullName: dbFullName, tableName: "users", diff --git a/dist/package-shared/functions/backend/handleNodemailer.d.ts b/dist/package-shared/functions/backend/handleNodemailer.d.ts index 89cee35..a0a6f2e 100644 --- a/dist/package-shared/functions/backend/handleNodemailer.d.ts +++ b/dist/package-shared/functions/backend/handleNodemailer.d.ts @@ -1,13 +1,11 @@ -type Param = { - to?: string; - subject?: string; - text?: string; - html?: string; +import Mail from "nodemailer/lib/mailer"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; +export type HandleNodemailerParam = Mail.Options & { senderName?: string; alias?: string | null; + options?: SMTPTransport.Options; }; /** * # Handle mails With Nodemailer */ -export default function handleNodemailer({ to, subject, text, html, alias, senderName, }: Param): Promise; -export {}; +export default function handleNodemailer(params: HandleNodemailerParam): Promise; diff --git a/dist/package-shared/functions/backend/handleNodemailer.js b/dist/package-shared/functions/backend/handleNodemailer.js index 5fa16d6..9d067fe 100644 --- a/dist/package-shared/functions/backend/handleNodemailer.js +++ b/dist/package-shared/functions/backend/handleNodemailer.js @@ -14,76 +14,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.default = handleNodemailer; const fs_1 = __importDefault(require("fs")); +const lodash_1 = __importDefault(require("lodash")); const nodemailer_1 = __importDefault(require("nodemailer")); -let transporter = nodemailer_1.default.createTransport({ - host: process.env.DSQL_MAIL_HOST, - port: 465, - secure: true, - auth: { - user: process.env.DSQL_MAIL_EMAIL, - pass: process.env.DSQL_MAIL_PASSWORD, - }, -}); /** * # Handle mails With Nodemailer */ -function handleNodemailer(_a) { - return __awaiter(this, arguments, void 0, function* ({ to, subject, text, html, alias, senderName, }) { - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// +function handleNodemailer(params) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b; if (!process.env.DSQL_MAIL_HOST || !process.env.DSQL_MAIL_EMAIL || !process.env.DSQL_MAIL_PASSWORD) { - return null; + return undefined; } + let transporter = nodemailer_1.default.createTransport(Object.assign({ host: process.env.DSQL_MAIL_HOST, port: 465, secure: true, auth: { + user: process.env.DSQL_MAIL_EMAIL, + pass: process.env.DSQL_MAIL_PASSWORD, + } }, params.options)); const sender = (() => { - if (alias === null || alias === void 0 ? void 0 : alias.match(/support/i)) + var _a; + if ((_a = params.alias) === null || _a === void 0 ? void 0 : _a.match(/support/i)) return process.env.DSQL_MAIL_EMAIL; return process.env.DSQL_MAIL_EMAIL; })(); - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// - let sentMessage; - if (!fs_1.default.existsSync("./email/index.html")) { - return; - } - let mailRoot = fs_1.default.readFileSync("./email/index.html", "utf8"); + const mailRootPath = process.env.DSQL_MAIL_ROOT || "./email/index.html"; + let mailRoot = fs_1.default.existsSync(mailRootPath) + ? fs_1.default.readFileSync(mailRootPath, "utf8") + : undefined; let finalHtml = mailRoot - .replace(/{{email_body}}/, html ? html : "") - .replace(/{{issue_date}}/, Date().substring(0, 24)); - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + ? mailRoot + .replace(/{{email_body}}/, ((_a = params.html) === null || _a === void 0 ? void 0 : _a.toString()) || "") + .replace(/{{issue_date}}/, Date().substring(0, 24)) + : (_b = params.html) === null || _b === void 0 ? void 0 : _b.toString(); try { let mailObject = {}; - mailObject["from"] = `"${senderName || "Datasquirel"}" <${sender}>`; + mailObject["from"] = `"${params.senderName || "Datasquirel"}" <${sender}>`; mailObject["sender"] = sender; - if (alias) + if (params.alias) mailObject["replyTo"] = sender; - mailObject["to"] = to; - mailObject["subject"] = subject; - mailObject["text"] = text; + mailObject["to"] = params.to; + mailObject["subject"] = params.subject; + mailObject["text"] = params.text; mailObject["html"] = finalHtml; - // send mail with defined transport object - let info = yield transporter.sendMail(mailObject); - sentMessage = info; - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + let info = yield transporter.sendMail(Object.assign(Object.assign({}, lodash_1.default.omit(mailObject, ["alias", "senderName", "options"])), mailObject)); + return info; } - catch ( /** @type {any} */error) { - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + catch (error) { console.log("ERROR in handleNodemailer Function =>", error.message); - // serverError({ - // component: "handleNodemailer", - // message: error.message, - // user: { email: to }, - // }); } - return sentMessage; + return undefined; }); } diff --git a/dist/package-shared/functions/email/fns/validate-email.d.ts b/dist/package-shared/functions/email/fns/validate-email.d.ts new file mode 100644 index 0000000..3358698 --- /dev/null +++ b/dist/package-shared/functions/email/fns/validate-email.d.ts @@ -0,0 +1,10 @@ +import { HandleNodemailerParam } from "../../backend/handleNodemailer"; +type Param = { + email?: string; + welcomeEmailOptions?: HandleNodemailerParam; +}; +export default function validateEmail({ email, welcomeEmailOptions, }: Param): Promise<{ + isValid: boolean; + message?: string; +}>; +export {}; diff --git a/dist/package-shared/functions/email/fns/validate-email.js b/dist/package-shared/functions/email/fns/validate-email.js new file mode 100644 index 0000000..a9ee328 --- /dev/null +++ b/dist/package-shared/functions/email/fns/validate-email.js @@ -0,0 +1,55 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = validateEmail; +const handleNodemailer_1 = __importDefault(require("../../backend/handleNodemailer")); +const email_mx_lookup_1 = __importDefault(require("../verification/email-mx-lookup")); +const email_regex_test_1 = __importDefault(require("../verification/email-regex-test")); +function validateEmail(_a) { + return __awaiter(this, arguments, void 0, function* ({ email, welcomeEmailOptions, }) { + var _b; + if (!email) { + return { + isValid: false, + message: "Email is required.", + }; + } + if (!(0, email_regex_test_1.default)(email)) { + return { + isValid: false, + message: "Invalid email format.", + }; + } + const checkEmailMxRecords = yield (0, email_mx_lookup_1.default)(email); + if (!checkEmailMxRecords) { + return { + isValid: false, + message: "Email domain does not have valid MX records.", + }; + } + if (welcomeEmailOptions) { + const welcomeEmail = yield (0, handleNodemailer_1.default)(welcomeEmailOptions); + if (!((_b = welcomeEmail === null || welcomeEmail === void 0 ? void 0 : welcomeEmail.accepted) === null || _b === void 0 ? void 0 : _b[0])) { + return { + isValid: false, + message: "Email verification failed.", + }; + } + } + return { + isValid: true, + message: "Email is valid.", + }; + }); +} diff --git a/dist/package-shared/functions/email/verification/email-mx-lookup.d.ts b/dist/package-shared/functions/email/verification/email-mx-lookup.d.ts new file mode 100644 index 0000000..d7cc4e3 --- /dev/null +++ b/dist/package-shared/functions/email/verification/email-mx-lookup.d.ts @@ -0,0 +1 @@ +export default function emailMxLookup(email?: string, debug?: boolean): Promise; diff --git a/dist/package-shared/functions/email/verification/email-mx-lookup.js b/dist/package-shared/functions/email/verification/email-mx-lookup.js new file mode 100644 index 0000000..de82543 --- /dev/null +++ b/dist/package-shared/functions/email/verification/email-mx-lookup.js @@ -0,0 +1,40 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = emailMxLookup; +const dns_1 = __importDefault(require("dns")); +const debug_log_1 = __importDefault(require("../../../utils/logging/debug-log")); +function emailMxLookup(email, debug) { + return new Promise((resolve, reject) => { + if (!email) { + resolve(false); + return; + } + const domain = email.split("@")[1]; + dns_1.default.resolveMx(domain, (err, addresses) => { + if (err || !addresses.length) { + if (debug) { + (0, debug_log_1.default)({ + log: (err === null || err === void 0 ? void 0 : err.message) || "No MX records found", + addTime: true, + label: "Email MX Lookup", + type: "error", + }); + } + resolve(false); + } + else { + if (debug) { + (0, debug_log_1.default)({ + log: addresses, + addTime: true, + label: "MX Records", + }); + } + resolve(true); + } + }); + }); +} diff --git a/dist/package-shared/functions/email/verification/email-regex-test.d.ts b/dist/package-shared/functions/email/verification/email-regex-test.d.ts new file mode 100644 index 0000000..e458c71 --- /dev/null +++ b/dist/package-shared/functions/email/verification/email-regex-test.d.ts @@ -0,0 +1 @@ +export default function emailRegexCheck(email: string): boolean; diff --git a/dist/package-shared/functions/email/verification/email-regex-test.js b/dist/package-shared/functions/email/verification/email-regex-test.js new file mode 100644 index 0000000..d954198 --- /dev/null +++ b/dist/package-shared/functions/email/verification/email-regex-test.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = emailRegexCheck; +function emailRegexCheck(email) { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); +} diff --git a/dist/package-shared/functions/email/verification/smtp-verification.d.ts b/dist/package-shared/functions/email/verification/smtp-verification.d.ts new file mode 100644 index 0000000..c52a7aa --- /dev/null +++ b/dist/package-shared/functions/email/verification/smtp-verification.d.ts @@ -0,0 +1 @@ +export default function verifyEmailSMTP(email: string): Promise; diff --git a/dist/package-shared/functions/email/verification/smtp-verification.js b/dist/package-shared/functions/email/verification/smtp-verification.js new file mode 100644 index 0000000..75b3c0e --- /dev/null +++ b/dist/package-shared/functions/email/verification/smtp-verification.js @@ -0,0 +1,44 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = verifyEmailSMTP; +const net_1 = __importDefault(require("net")); +const dns_1 = __importDefault(require("dns")); +function verifyEmailSMTP(email) { + return new Promise((resolve, reject) => { + const domain = email.split("@")[1]; + dns_1.default.resolveMx(domain, (err, addresses) => { + if (err || !addresses.length) { + console.log("Invalid email domain."); + return; + } + const mxServer = addresses[0].exchange; + console.log(`Connecting to ${mxServer} to verify email...`); + const client = net_1.default.createConnection(25, mxServer); + client.on("connect", () => { + console.log("Connected to SMTP server."); + client.write("HELO example.com\r\n"); + client.write(`MAIL FROM: \r\n`); + client.write(`RCPT TO: <${email}>\r\n`); + }); + client.on("data", (data) => { + const response = data.toString(); + if (response.includes("250")) { + console.log("✅ Email exists!"); + resolve(true); + } + else { + console.log("❌ Email does not exist."); + resolve(false); + } + client.end(); + }); + client.on("error", (err) => { + console.log("SMTP verification failed:", err.message); + resolve(false); + }); + }); + }); +} diff --git a/package-shared/actions/users/login-user.ts b/package-shared/actions/users/login-user.ts index f1ffb79..689cecf 100644 --- a/package-shared/actions/users/login-user.ts +++ b/package-shared/actions/users/login-user.ts @@ -12,8 +12,10 @@ import { PackageUserLoginRequestBody, } from "../../types"; import debugLog from "../../utils/logging/debug-log"; -import numberfy from "../../utils/numberfy"; import grabCookieExpiryDate from "../../utils/grab-cookie-expirt-date"; +import emailRegexCheck from "../../functions/email/verification/email-regex-test"; +import emailMxLookup from "../../functions/email/verification/email-mx-lookup"; +import validateEmail from "../../functions/email/fns/validate-email"; type Param = { key?: string; @@ -24,6 +26,7 @@ type Param = { password?: string; }; additionalFields?: string[]; + request?: http.IncomingMessage & { [s: string]: any }; response?: http.ServerResponse & { [s: string]: any }; encryptionKey?: string; encryptionSalt?: string; @@ -64,6 +67,7 @@ export default async function loginUser({ debug, cleanupTokens, secureCookie, + request, }: Param): Promise { const grabedHostNames = grabHostNames({ userId: user_id || apiUserID }); const { host, port, scheme } = grabedHostNames; @@ -108,11 +112,13 @@ export default async function loginUser({ * * @description Check required fields */ - if (!payload.email) { + const isEmailValid = await validateEmail({ email: payload.email }); + + if (!payload.email || !isEmailValid.isValid) { return { success: false, payload: null, - msg: "Email Required", + msg: isEmailValid.message, }; } diff --git a/package-shared/external-services/arcjet/index.ts b/package-shared/external-services/arcjet/index.ts new file mode 100644 index 0000000..9a6c52a --- /dev/null +++ b/package-shared/external-services/arcjet/index.ts @@ -0,0 +1,31 @@ +// import arcjet, { ArcjetOptions, Primitive, Product } from "@arcjet/node"; + +// interface Params< +// Rules extends (Primitive | Product)[], +// Characteristics extends readonly string[] +// > { +// options?: Omit, "key" | "rules"> & { +// rules?: Rules; +// }; +// } + +// export default function arcjetClient< +// Rules extends (Primitive | Product)[], +// Characteristics extends readonly string[] +// >(params?: Params) { +// const ARCJET_KEY = process.env.DSQL_ARCJET_KEY; +// const ARCJET_ENV = process.env.NODE_ENV || "development"; + +// if (!ARCJET_KEY) { +// return null; +// } + +// const aj = arcjet({ +// key: ARCJET_KEY, +// characteristics: ["ip.src"], +// rules: [], +// ...params?.options, +// }); + +// return aj; +// } diff --git a/package-shared/functions/api/users/api-create-user.ts b/package-shared/functions/api/users/api-create-user.ts index 8d620aa..3685fc0 100644 --- a/package-shared/functions/api/users/api-create-user.ts +++ b/package-shared/functions/api/users/api-create-user.ts @@ -6,6 +6,7 @@ import addDbEntry from "../../backend/db/addDbEntry"; import updateUsersTableSchema from "../../backend/updateUsersTableSchema"; import varDatabaseDbHandler from "../../backend/varDatabaseDbHandler"; import hashPassword from "../../dsql/hashPassword"; +import validateEmail from "../../email/fns/validate-email"; /** * # API Create User @@ -118,6 +119,16 @@ export default async function apiCreateUser({ }; } + const isEmailValid = await validateEmail({ email: payload.email }); + + if (!isEmailValid.isValid) { + return { + success: false, + msg: isEmailValid.message, + payload: null, + }; + } + const addUser = await addDbEntry({ dbFullName: dbFullName, tableName: "users", diff --git a/package-shared/functions/backend/handleNodemailer.ts b/package-shared/functions/backend/handleNodemailer.ts index 47fea33..df667cd 100644 --- a/package-shared/functions/backend/handleNodemailer.ts +++ b/package-shared/functions/backend/handleNodemailer.ts @@ -1,103 +1,79 @@ import fs from "fs"; +import _ from "lodash"; import nodemailer from "nodemailer"; +import Mail from "nodemailer/lib/mailer"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; -let transporter = nodemailer.createTransport({ - host: process.env.DSQL_MAIL_HOST, - port: 465, - secure: true, - auth: { - user: process.env.DSQL_MAIL_EMAIL, - pass: process.env.DSQL_MAIL_PASSWORD, - }, -}); - -type Param = { - to?: string; - subject?: string; - text?: string; - html?: string; +export type HandleNodemailerParam = Mail.Options & { senderName?: string; alias?: string | null; + options?: SMTPTransport.Options; }; /** * # Handle mails With Nodemailer */ -export default async function handleNodemailer({ - to, - subject, - text, - html, - alias, - senderName, -}: Param): Promise { - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// - +export default async function handleNodemailer( + params: HandleNodemailerParam +): Promise { if ( !process.env.DSQL_MAIL_HOST || !process.env.DSQL_MAIL_EMAIL || !process.env.DSQL_MAIL_PASSWORD ) { - return null; + return undefined; } + let transporter = nodemailer.createTransport({ + host: process.env.DSQL_MAIL_HOST, + port: 465, + secure: true, + auth: { + user: process.env.DSQL_MAIL_EMAIL, + pass: process.env.DSQL_MAIL_PASSWORD, + }, + ...params.options, + }); + const sender = (() => { - if (alias?.match(/support/i)) return process.env.DSQL_MAIL_EMAIL; + if (params.alias?.match(/support/i)) return process.env.DSQL_MAIL_EMAIL; return process.env.DSQL_MAIL_EMAIL; })(); - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + const mailRootPath = process.env.DSQL_MAIL_ROOT || "./email/index.html"; - let sentMessage; + let mailRoot = fs.existsSync(mailRootPath) + ? fs.readFileSync(mailRootPath, "utf8") + : undefined; - if (!fs.existsSync("./email/index.html")) { - return; - } - - let mailRoot = fs.readFileSync("./email/index.html", "utf8"); let finalHtml = mailRoot - .replace(/{{email_body}}/, html ? html : "") - .replace(/{{issue_date}}/, Date().substring(0, 24)); - - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + ? mailRoot + .replace(/{{email_body}}/, params.html?.toString() || "") + .replace(/{{issue_date}}/, Date().substring(0, 24)) + : params.html?.toString(); try { let mailObject: any = {}; - mailObject["from"] = `"${senderName || "Datasquirel"}" <${sender}>`; + mailObject["from"] = `"${ + params.senderName || "Datasquirel" + }" <${sender}>`; mailObject["sender"] = sender; - if (alias) mailObject["replyTo"] = sender; - mailObject["to"] = to; - mailObject["subject"] = subject; - mailObject["text"] = text; + if (params.alias) mailObject["replyTo"] = sender; + mailObject["to"] = params.to; + mailObject["subject"] = params.subject; + mailObject["text"] = params.text; mailObject["html"] = finalHtml; - // send mail with defined transport object - let info = await transporter.sendMail(mailObject); - - sentMessage = info; - - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// - } catch (/** @type {any} */ error: any) { - //////////////////////////////////////// - //////////////////////////////////////// - //////////////////////////////////////// + let info = await transporter.sendMail({ + ..._.omit(mailObject, ["alias", "senderName", "options"]), + ...mailObject, + }); + return info; + } catch (error: any) { console.log("ERROR in handleNodemailer Function =>", error.message); - // serverError({ - // component: "handleNodemailer", - // message: error.message, - // user: { email: to }, - // }); } - return sentMessage; + return undefined; } diff --git a/package-shared/functions/email/fns/validate-email.ts b/package-shared/functions/email/fns/validate-email.ts new file mode 100644 index 0000000..a55b50c --- /dev/null +++ b/package-shared/functions/email/fns/validate-email.ts @@ -0,0 +1,52 @@ +import handleNodemailer, { + HandleNodemailerParam, +} from "../../backend/handleNodemailer"; +import emailMxLookup from "../verification/email-mx-lookup"; +import emailRegexCheck from "../verification/email-regex-test"; + +type Param = { + email?: string; + welcomeEmailOptions?: HandleNodemailerParam; +}; +export default async function validateEmail({ + email, + welcomeEmailOptions, +}: Param): Promise<{ isValid: boolean; message?: string }> { + if (!email) { + return { + isValid: false, + message: "Email is required.", + }; + } + + if (!emailRegexCheck(email)) { + return { + isValid: false, + message: "Invalid email format.", + }; + } + + const checkEmailMxRecords = await emailMxLookup(email); + + if (!checkEmailMxRecords) { + return { + isValid: false, + message: "Email domain does not have valid MX records.", + }; + } + + if (welcomeEmailOptions) { + const welcomeEmail = await handleNodemailer(welcomeEmailOptions); + if (!welcomeEmail?.accepted?.[0]) { + return { + isValid: false, + message: "Email verification failed.", + }; + } + } + + return { + isValid: true, + message: "Email is valid.", + }; +} diff --git a/package-shared/functions/email/verification/email-mx-lookup.ts b/package-shared/functions/email/verification/email-mx-lookup.ts new file mode 100644 index 0000000..e14533d --- /dev/null +++ b/package-shared/functions/email/verification/email-mx-lookup.ts @@ -0,0 +1,39 @@ +import dns from "dns"; +import debugLog from "../../../utils/logging/debug-log"; + +export default function emailMxLookup( + email?: string, + debug?: boolean +): Promise { + return new Promise((resolve, reject) => { + if (!email) { + resolve(false); + return; + } + + const domain = email.split("@")[1]; + + dns.resolveMx(domain, (err, addresses) => { + if (err || !addresses.length) { + if (debug) { + debugLog({ + log: err?.message || "No MX records found", + addTime: true, + label: "Email MX Lookup", + type: "error", + }); + } + resolve(false); + } else { + if (debug) { + debugLog({ + log: addresses, + addTime: true, + label: "MX Records", + }); + } + resolve(true); + } + }); + }); +} diff --git a/package-shared/functions/email/verification/email-regex-test.ts b/package-shared/functions/email/verification/email-regex-test.ts new file mode 100644 index 0000000..a82b971 --- /dev/null +++ b/package-shared/functions/email/verification/email-regex-test.ts @@ -0,0 +1,4 @@ +export default function emailRegexCheck(email: string): boolean { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); +} diff --git a/package-shared/functions/email/verification/smtp-verification.ts b/package-shared/functions/email/verification/smtp-verification.ts new file mode 100644 index 0000000..37aaf3b --- /dev/null +++ b/package-shared/functions/email/verification/smtp-verification.ts @@ -0,0 +1,46 @@ +import net from "net"; +import dns from "dns"; + +export default function verifyEmailSMTP(email: string): Promise { + return new Promise((resolve, reject) => { + const domain = email.split("@")[1]; + + dns.resolveMx(domain, (err, addresses) => { + if (err || !addresses.length) { + console.log("Invalid email domain."); + return; + } + + const mxServer = addresses[0].exchange; + console.log(`Connecting to ${mxServer} to verify email...`); + + const client = net.createConnection(25, mxServer); + + client.on("connect", () => { + console.log("Connected to SMTP server."); + client.write("HELO example.com\r\n"); + client.write(`MAIL FROM: \r\n`); + client.write(`RCPT TO: <${email}>\r\n`); + }); + + client.on("data", (data) => { + const response = data.toString(); + + if (response.includes("250")) { + console.log("✅ Email exists!"); + resolve(true); + } else { + console.log("❌ Email does not exist."); + resolve(false); + } + + client.end(); + }); + + client.on("error", (err) => { + console.log("SMTP verification failed:", err.message); + resolve(false); + }); + }); + }); +} diff --git a/package.json b/package.json index c84c8c4..7603329 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/datasquirel", - "version": "4.2.6", + "version": "4.2.7", "description": "Cloud-based SQL data management tool", "main": "dist/index.js", "bin": { @@ -29,6 +29,15 @@ }, "homepage": "https://datasquirel.com/", "dependencies": { + "@types/ace": "^0.0.52", + "@types/lodash": "^4.17.13", + "@types/mysql": "^2.15.21", + "@types/next": "^9.0.0", + "@types/node": "^22.7.5", + "@types/nodemailer": "^6.4.17", + "@types/react": "^18.2.21", + "@types/react-dom": "^19.0.0", + "@types/tinymce": "^4.6.9", "dotenv": "^16.3.1", "generate-password": "^1.7.1", "google-auth-library": "^9.15.0", @@ -38,15 +47,6 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "sanitize-html": "^2.13.1", - "serverless-mysql": "^1.5.5", - "@types/nodemailer": "^6.4.17", - "@types/ace": "^0.0.52", - "@types/lodash": "^4.17.13", - "@types/mysql": "^2.15.21", - "@types/next": "^9.0.0", - "@types/node": "^22.7.5", - "@types/react": "^18.2.21", - "@types/react-dom": "^19.0.0", - "@types/tinymce": "^4.6.9" + "serverless-mysql": "^1.5.5" } }