初始化提交:go比特版本精简
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
'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 } };
|
||||
Reference in New Issue
Block a user