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, };