Files
crm-ai/services/imap-id-proxy/server.js
T
2026-10-23 10:35:28 +08:00

119 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* IMAP ID 注入代理(专治 163 "Unsafe Login")
*
* 背景:LibreDesk 的 go-imap 客户端不发 IMAP ID 命令,163 会在 SELECT 时返回
* "NO SELECT Unsafe Login",导致邮箱无法收取。
*
* 本代理:明文监听 1993,TLS 连接上游 imap.163.com:993;
* 在客户端发 SELECT/EXAMINE 之前,自动发送一条 ID 命令并吞掉它的响应。
* 仅用于本地/内网 POC,不要在公网暴露。
*/
const net = require('net');
const tls = require('tls');
const LISTEN_PORT = Number(process.env.LISTEN_PORT || 1993);
const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'imap.163.com';
const UPSTREAM_PORT = Number(process.env.UPSTREAM_PORT || 993);
const ID_LINE = process.env.ID_LINE || '("name" "libredesk" "version" "2.9" "vendor" "libredesk")';
let connSeq = 0;
function log(...args) {
console.log(new Date().toISOString(), '[imap-id-proxy]', ...args);
}
function pipeLineSplitter(onLine) {
let buf = '';
return {
push(chunk) {
buf += chunk.toString('latin1');
let idx;
while ((idx = buf.indexOf('\r\n')) >= 0) {
const line = buf.slice(0, idx);
buf = buf.slice(idx + 2);
onLine(line);
}
},
};
}
const server = net.createServer((client) => {
const id = ++connSeq;
const tag = `P${id}`;
let injected = false;
let waitingId = false;
const pendingFromClient = [];
let upstreamWrite = null;
log(`conn ${id} opened`);
const upstream = tls.connect({ host: UPSTREAM_HOST, port: UPSTREAM_PORT, servername: UPSTREAM_HOST }, () => {
log(`conn ${id} upstream TLS ready`);
});
upstream.setEncoding('latin1');
client.setEncoding('latin1');
upstreamWrite = (line) => upstream.write(`${line}\r\n`);
const clientLines = pipeLineSplitter((line) => {
const upper = line.toUpperCase();
const isMailboxCmd = /^\S+\s+(SELECT|EXAMINE)\b/.test(upper);
if (!injected && isMailboxCmd) {
injected = true;
waitingId = true;
pendingFromClient.push(line);
log(`conn ${id} injecting ID before: ${line.slice(0, 40)}`);
upstream.write(`${tag} ID ${ID_LINE}\r\n`);
return;
}
if (waitingId) {
pendingFromClient.push(line);
return;
}
upstream.write(`${line}\r\n`);
});
const upstreamLines = pipeLineSplitter((line) => {
if (waitingId) {
if (line.startsWith('* ID ')) return; // 吞掉 untagged ID 响应
if (/^P\d+\s+(OK|NO|BAD)\b/i.test(line)) {
const ok = /^P\d+\s+OK\b/i.test(line);
log(`conn ${id} ID response: ${line.slice(0, 60)}`);
waitingId = false;
if (!ok) {
// ID 被拒也继续转发,让上层自己报错
client.write(`${line}\r\n`);
}
while (pendingFromClient.length) upstream.write(`${pendingFromClient.shift()}\r\n`);
return;
}
// 其它服务器响应(如 * OK)先转发
client.write(`${line}\r\n`);
return;
}
client.write(`${line}\r\n`);
});
client.on('data', (chunk) => clientLines.push(chunk));
upstream.on('data', (chunk) => upstreamLines.push(chunk));
const close = (why) => {
log(`conn ${id} closed (${why})`);
client.destroy();
upstream.destroy();
};
client.on('error', (e) => close(`client error ${e.message}`));
upstream.on('error', (e) => {
log(`conn ${id} upstream error ${e.message}`);
client.destroy();
});
client.on('close', () => upstream.destroy());
upstream.on('close', () => client.destroy());
});
server.listen(LISTEN_PORT, '0.0.0.0', () => {
log(`listening :${LISTEN_PORT} -> ${UPSTREAM_HOST}:${UPSTREAM_PORT} (TLS)`);
});