Files
go_bt/scripts/test_gmail_imap.js
T

149 lines
5.5 KiB
JavaScript

'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 <email>');
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 } };