'use strict'; const tls = require('tls'); const db = require('../src/core/database'); const IMAP_HOST = 'imap.gmail.com'; const IMAP_PORT = 993; function quoteImap(value) { return `"${String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; } function decodeMimeWords(value) { return String(value || '').replace(/=\?([^?]+)\?([bq])\?([^?]*)\?=/gi, (_, charset, encoding, data) => { try { if (encoding.toLowerCase() === 'b') return Buffer.from(data, 'base64').toString('utf8'); const bytes = data.replace(/_/g, ' ').replace(/=([0-9a-f]{2})/gi, (_m, hex) => String.fromCharCode(parseInt(hex, 16))); return Buffer.from(bytes, 'binary').toString(/^utf-?8$/i.test(charset) ? 'utf8' : 'latin1'); } catch (_) { return data; } }); } function headerValue(block, name) { const unfolded = String(block || '').replace(/\r?\n[\t ]+/g, ' '); const match = unfolded.match(new RegExp(`(?:^|\\r?\\n)${name}:\\s*([^\\r\\n]*)`, 'i')); return match ? decodeMimeWords(match[1].trim()) : ''; } function recentHeaders(fetchResponse) { const blocks = String(fetchResponse || '').split(/(?=\* \d+ FETCH \()/i); return blocks.map(block => { const uid = (block.match(/\bUID\s+(\d+)/i) || [])[1]; if (!uid) return null; return { uid: Number(uid), from: headerValue(block, 'From'), subject: headerValue(block, 'Subject'), date: headerValue(block, 'Date'), }; }).filter(Boolean); } async function connectImap(email, appPassword) { return new Promise((resolve, reject) => { const socket = tls.connect({ host: IMAP_HOST, port: IMAP_PORT, servername: IMAP_HOST, rejectUnauthorized: true, }); socket.setTimeout(30000); let buffer = ''; let greetingSeen = false; let settled = false; const waiters = new Map(); const finishError = error => { if (settled) return; settled = true; try { socket.destroy(); } catch (_) {} reject(error); }; socket.on('timeout', () => finishError(new Error('IMAP连接或命令响应超时'))); socket.on('error', finishError); socket.on('data', chunk => { buffer += chunk.toString('utf8'); if (!greetingSeen && /^\* (?:OK|PREAUTH)/m.test(buffer)) greetingSeen = true; for (const [tag, waiter] of waiters) { const match = buffer.match(new RegExp(`(?:^|\\r\\n)${tag} (OK|NO|BAD)(?: |\\r\\n)`, 'i')); if (!match) continue; const end = match.index + match[0].length; const response = buffer.slice(waiter.start, end); waiters.delete(tag); if (match[1].toUpperCase() === 'OK') waiter.resolve(response); else waiter.reject(new Error(`IMAP命令失败:${match[1].toUpperCase()}`)); } }); socket.once('secureConnect', async () => { try { const greetingDeadline = Date.now() + 10000; while (!greetingSeen && Date.now() < greetingDeadline) { await new Promise(r => setTimeout(r, 50)); } if (!greetingSeen) throw new Error('未收到 Gmail IMAP 欢迎响应'); let sequence = 0; const command = text => new Promise((commandResolve, commandReject) => { const tag = `A${String(++sequence).padStart(3, '0')}`; waiters.set(tag, { start: buffer.length, resolve: commandResolve, reject: commandReject }); socket.write(`${tag} ${text}\r\n`); }); await command(`LOGIN ${quoteImap(email)} ${quoteImap(appPassword)}`); const statusResponse = await command('STATUS INBOX (MESSAGES UNSEEN)'); await command('EXAMINE INBOX'); const searchResponse = await command('UID SEARCH ALL'); const searchLine = (searchResponse.match(/\* SEARCH([^\r\n]*)/i) || [])[1] || ''; const uids = searchLine.trim().split(/\s+/).filter(value => /^\d+$/.test(value)); const latestUids = uids.slice(-3); let headers = []; if (latestUids.length) { const fetchResponse = await command(`UID FETCH ${latestUids.join(',')} (UID BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])`); headers = recentHeaders(fetchResponse).sort((a, b) => b.uid - a.uid); } await command('LOGOUT').catch(() => {}); const statusItems = (statusResponse.match(/\* STATUS\s+"?INBOX"?\s+\(([^)]*)\)/i) || [])[1] || ''; const messageCount = (statusItems.match(/\bMESSAGES\s+(\d+)/i) || [])[1]; const unseenCount = (statusItems.match(/\bUNSEEN\s+(\d+)/i) || [])[1]; settled = true; socket.end(); resolve({ ok: true, host: IMAP_HOST, port: IMAP_PORT, tlsAuthorized: socket.authorized, messages: messageCount ? Number(messageCount) : uids.length, unseen: unseenCount ? Number(unseenCount) : null, recentHeaders: headers, }); } catch (error) { finishError(error); } }); }); } async function main() { const email = String(process.argv[2] || '').trim(); if (!email) throw new Error('用法: node scripts/test_gmail_imap.js '); db.initDB(); const saved = db.getGmailAppPassword(email); if (!saved || !saved.app_password) throw new Error('数据库中没有该邮箱的16位应用专用密码'); const result = await connectImap(email, saved.app_password); console.log(JSON.stringify({ email, ...result }, null, 2)); } main().catch(error => { console.error(JSON.stringify({ ok: false, message: error.message })); process.exitCode = 1; }); module.exports = { connectImap, _private: { decodeMimeWords, recentHeaders } };