Files
2026-06-28 19:11:38 +05:30

334 lines
8.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
let ImapFlow;
try {
({ ImapFlow } = require('imapflow'));
} catch (_) {
ImapFlow = null;
}
function roleKey(roleText='') {
const r = String(roleText || '').toLowerCase();
if (r.includes('tenant b')) return 'TENANT_B_ADMIN';
if (r.includes('system')) return 'SYSTEM_ADMIN';
if (r.includes('firm admin')) return 'FIRM_ADMIN';
if (r.includes('branch') && r.includes('staff')) return 'BRANCH2_STAFF';
if (r.includes('branch manager')) return 'MANAGER';
if (r.includes('partner')) return 'PARTNER';
if (r.includes('manager')) return 'MANAGER';
if (r.includes('staff2')) return 'STAFF2';
if (r.includes('staff') || r.includes('employee')) return 'STAFF';
if (r.includes('client2')) return 'CLIENT2';
if (r.includes('client')) return 'CLIENT';
if (r.includes('consultant')) return 'CONSULTANT';
if (r.includes('invite')) return 'INVITE_TEST';
if (r.includes('reset')) return 'RESET_TEST';
if (r.includes('locked')) return 'LOCKED_USER';
return 'SYSTEM_ADMIN';
}
function roleCredentials(roleText='') {
const key = roleKey(roleText);
return [process.env[`${key}_EMAIL`], process.env[`${key}_PASSWORD`]];
}
function envBool(name, defaultValue = false) {
const value = process.env[name];
if (value === undefined || value === null || value === '') return defaultValue;
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
}
function readOtpFromFile() {
const otpPath = path.resolve('results/current-otp.txt');
if (fs.existsSync(otpPath)) return fs.readFileSync(otpPath, 'utf8').trim();
return '';
}
function firstOtpMatch(text) {
const value = String(text || '');
// First try custom regex from environment, if provided.
if (process.env.IMAP_OTP_REGEX) {
try {
const flags = process.env.IMAP_OTP_REGEX_FLAGS || 'm';
const regex = new RegExp(process.env.IMAP_OTP_REGEX, flags);
const match = value.match(regex);
if (match) return match[1] || match[0];
} catch (_) {
// Fall back to default 6-digit OTP regex.
}
}
// Reliable fallback for ERP OTP mails.
const fallback = value.match(/\b\d{6}\b/);
return fallback ? fallback[0] : '';
}
function imapConfigForRole(roleText='', email='') {
const key = roleKey(roleText);
const user = process.env[`${key}_IMAP_USER`] || email || process.env.IMAP_USER;
const password = process.env[`${key}_IMAP_PASSWORD`] || process.env.IMAP_PASSWORD;
return {
host: process.env.IMAP_HOST,
port: Number(process.env.IMAP_PORT || 993),
secure: envBool('IMAP_SECURE', true),
auth: { user, pass: password },
mailbox: process.env.IMAP_MAILBOX || 'INBOX',
searchFrom: process.env[`${key}_IMAP_SEARCH_FROM`] || process.env.IMAP_SEARCH_FROM || '',
waitSeconds: Number(process.env.IMAP_WAIT_SECONDS || 60),
lookbackMinutes: Number(process.env.IMAP_LOOKBACK_MINUTES || 10),
maxMessages: Number(process.env.IMAP_MAX_MESSAGES || 20),
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchOtpOnceFromImap(config, sinceDate) {
if (!ImapFlow) {
throw new Error('OTP_FROM_IMAP=true but package "imapflow" is not installed. Run: npm install');
}
if (!config.host || !config.auth.user || !config.auth.pass) {
throw new Error('Missing IMAP_HOST, role IMAP user, or role IMAP password for OTP fetch.');
}
const client = new ImapFlow({
host: config.host,
port: config.port,
secure: config.secure,
auth: config.auth,
logger: false,
});
await client.connect();
try {
const expectedFrom = String(
config.searchFrom || process.env.IMAP_SEARCH_FROM || 'no-reply@vavalam.com'
).trim().toLowerCase();
const mailboxes = [
config.mailbox || 'INBOX',
'Junk Mail',
'Deleted Items',
];
const seen = new Set();
for (const mailbox of mailboxes) {
if (seen.has(mailbox)) continue;
seen.add(mailbox);
let lock;
try {
lock = await client.getMailboxLock(mailbox);
const matches = [];
for await (const msg of client.fetch(
'1:*',
{ envelope: true, source: true, uid: true },
{ uid: false }
)) {
const from = msg.envelope?.from?.map(x => x.address).join(', ').toLowerCase() || '';
const subject = msg.envelope?.subject || '';
const sourceText = msg.source ? msg.source.toString('utf8') : '';
const envelopeText = JSON.stringify(msg.envelope || {});
const fullText = envelopeText + '\n' + sourceText;
if (
expectedFrom &&
!from.includes(expectedFrom) &&
!fullText.toLowerCase().includes(expectedFrom)
) {
continue;
}
const otp = firstOtpMatch(fullText);
if (!otp) continue;
matches.push({
uid: msg.uid || 0,
date: msg.envelope?.date ? new Date(msg.envelope.date).getTime() : 0,
otp,
subject,
from,
});
}
matches.sort((a, b) => {
if ((b.date || 0) !== (a.date || 0)) {
return (b.date || 0) - (a.date || 0);
}
return (b.uid || 0) - (a.uid || 0);
});
if (matches[0]?.otp) {
return matches[0].otp;
}
} catch (error) {
if (envBool('IMAP_FAIL_FAST', false)) {
throw error;
}
// Try next mailbox.
} finally {
if (lock) lock.release();
}
}
return '';
} finally {
try {
await client.logout();
} catch (_) {}
}
}
async function getOtp(roleText='', email='') {
if (process.env.STATIC_OTP) return process.env.STATIC_OTP;
const fileOtp = readOtpFromFile();
if (fileOtp) return fileOtp;
if (!envBool('OTP_FROM_IMAP', false)) return '';
const config = imapConfigForRole(roleText, email);
const startedAt = Date.now();
const sinceDate = new Date(startedAt - config.lookbackMinutes * 60 * 1000);
const deadline = startedAt + config.waitSeconds * 1000;
let lastError;
while (Date.now() <= deadline) {
try {
const otp = await fetchOtpOnceFromImap(config, sinceDate);
if (otp) return otp;
} catch (error) {
lastError = error;
if (envBool('IMAP_FAIL_FAST', false)) throw error;
}
await sleep(3000);
}
if (lastError && envBool('IMAP_THROW_ON_TIMEOUT', false)) {
throw lastError;
}
return '';
}
async function fillFirst(page, selectors, value) {
for (const sel of selectors) {
const loc = page.locator(sel).first();
if (await loc.count()) {
try {
await loc.fill(value);
return true;
} catch (_) {}
}
}
return false;
}
async function clickFirst(page, selectors) {
for (const sel of selectors) {
const loc = page.locator(sel).first();
if (await loc.count()) {
try {
await loc.click();
return true;
} catch (_) {}
}
}
return false;
}
async function login(page, roleText='System Admin') {
const [email, password] = roleCredentials(roleText);
await page.goto('/login');
await fillFirst(
page,
[
'input[name="email"]',
'input[name="login_id"]',
'input[type="email"]',
'input[name="username"]',
'input[name="user"]',
],
email || ''
);
await fillFirst(
page,
[
'input[name="password"]',
'input[type="password"]',
],
password || ''
);
await clickFirst(
page,
[
'button[type="submit"]',
'input[type="submit"]',
'text=/login/i',
]
);
await page.waitForLoadState('domcontentloaded');
if (page.url().includes('/otp')) {
const otp = await getOtp(roleText, email);
if (otp) {
await fillFirst(
page,
[
'input[name="otp"]',
'input[name="code"]',
'input[type="text"]',
],
otp
);
await clickFirst(
page,
[
'button[type="submit"]',
'input[type="submit"]',
'text=/verify/i',
]
);
await page.waitForLoadState('domcontentloaded');
}
}
}
async function logout(page) {
try {
await page.goto('/logout');
await page.waitForLoadState('domcontentloaded');
} catch (_) {}
}
module.exports = {
login,
logout,
roleCredentials,
roleKey,
getOtp,
fillFirst,
clickFirst,
};