mgimap
Version:
node imap proxy
499 lines • 17.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.simpleParser = exports.MgImap = void 0;
const events_1 = require("events");
const net_1 = require("net");
const tls = require("tls");
const socks_1 = require("socks");
const Parser_1 = require("./Parser");
const mailparser_1 = require("mailparser");
Object.defineProperty(exports, "simpleParser", { enumerable: true, get: function () { return mailparser_1.simpleParser; } });
const buildSearchQuery = require("./funs").buildSearchQuery;
const utf7 = require("utf7").imap;
class MgImap extends events_1.EventEmitter {
constructor(opts) {
super();
this.socketTimeout = 5 * 1000;
this.tagHandlerMap = new Map();
this.tagNum = 0;
this.cmdQueue = [];
this.currCmd = "";
this.searchUids = [];
this.caps = [];
this.idling = false;
this.logined = false;
this.box = {
name: "",
flags: [],
readOnly: false,
uidvalidity: 0,
uidnext: 0,
permFlags: [],
keywords: [],
newKeywords: false,
persistentUIDs: true,
nomodseq: false,
messages: {
total: 0,
new: 0,
},
};
this.options = opts;
if (opts.socketTimeout) {
this.socketTimeout = opts.socketTimeout;
}
}
async connect() {
if (!this.socket) {
const { host, port, logger, tlsPort, proxy, tls } = this.options;
this.initParser();
if (proxy) {
try {
const info = await socks_1.SocksClient.createConnection({
proxy: {
host: proxy.host,
port: proxy.port,
userId: proxy.username,
password: proxy.password,
type: proxy.type || 5,
},
command: "connect",
destination: {
host,
port: tlsPort,
},
});
this.createTLS(info.socket).then(() => {
this.handleConnect(info.socket);
}).catch(() => {
this.emit("socketError", new Error("Tls error"));
});
}
catch (err) {
logger && logger("proxy error", err);
this.emit("proxyError", new Error("Proxy connect error " + err.message));
return;
}
}
else {
this.socket = new net_1.Socket();
this.setSocketEvent();
this.socket.connect({
host,
port: tls ? tlsPort : port,
}, () => {
if (tls) {
this.createTLS(this.socket).then(() => {
this.handleConnect(this.socket);
}).catch(err => {
logger && logger("tls error", err);
this.emit("tlsError", new Error("TLS connect error " + err.message));
});
}
else {
this.handleConnect(this.socket);
}
});
}
}
}
/**
* 发送指令
* @param cmd
* @param callback
*/
sendCmd(cmd, callback) {
const { logger } = this.options;
const scmd = `A${++this.tagNum} ${cmd}\r\n`;
if (this.currCmd === "") {
this.socket?.write(scmd);
if (callback) {
this.tagHandlerMap.set(this.tagNum, callback);
}
this.currCmd = cmd;
logger && logger("=>", scmd);
}
else {
this.cmdQueue.push({ cmd, callback });
}
}
/**
* 登录
* @returns
*/
async login() {
const { user, password } = this.options;
return new Promise((resolve) => {
if (this.logined) {
resolve(true);
return;
}
this.sendCmd(`LOGIN "${user}" "${password}"`, (res) => {
if (res.result === "ok") {
this.logined = true;
this.emit("login", true);
resolve(true);
}
else {
this.emit("login", false, res.text);
resolve(false);
}
});
});
}
async ID(identification) {
var cmd = 'ID';
if ((identification === null) || (Object.keys(identification).length === 0))
cmd += ' NIL';
else {
if (Object.keys(identification).length > 30)
throw new Error('Max allowed number of keys is 30');
var kv = [];
for (var k in identification) {
if (Buffer.byteLength(k) > 30)
throw new Error('Max allowed key length is 30');
if (Buffer.byteLength(identification[k]) > 1024)
throw new Error('Max allowed value length is 1024');
kv.push('"' + encodeURI(k) + '"');
kv.push('"' + encodeURI(identification[k]) + '"');
}
cmd += ' (' + kv.join(' ') + ')';
}
return new Promise((resolve) => {
this.sendCmd(cmd, resolve);
});
}
/**
* 选择邮箱文件夹
* @param name
* @param readOnly
* @param callback
*/
async openBox(name, readOnly) {
name = "" + name;
let encname = encodeURI(utf7.encode(name)), cmd = readOnly ? "EXAMINE" : "SELECT";
return new Promise((resolve, reject) => {
this.sendCmd(`${cmd} "${encname}"`, (res) => {
if (res.result === "ok") {
this.box.name = name;
this.box.readOnly = !!readOnly;
resolve(this.box);
}
else {
reject(res.text);
}
});
});
}
/**
* 用uid搜索邮箱
* @param range ALL 100:200, 100:*
*/
async searchUid(range) {
return new Promise((resolve, reject) => {
this.sendCmd(`UID SEARCH UID ${range}`, (res) => {
if (res.result === "ok") {
resolve(this.searchUids);
}
else {
reject(this.currCmd + "\r\n" + res.text);
}
});
});
}
/**
* 条件查询
*/
async search(criteria) {
var cmd = 'UID SEARCH', info = { hasUTF8: false /*output*/ }, query = buildSearchQuery(criteria, this.caps, info);
// lines;
// if (info.hasUTF8) {
// cmd += ' CHARSET UTF-8';
// lines = query.split("\r\n");
// query = lines.shift();
// }
cmd += query;
return new Promise((resolve, reject) => {
this.sendCmd(cmd, (res) => {
if (res.result === "ok") {
resolve(this.searchUids);
}
else {
reject(this.currCmd + "\r\n" + res.text);
}
});
});
}
/**
* 读取uid的邮件内容
* @param range
*/
async fetchUid(range) {
return new Promise((resolve, reject) => {
this.sendCmd(`UID FETCH ${range.join(",")} (UID FLAGS INTERNALDATE BODYSTRUCTURE BODY[])`, (res) => {
if (res.result === "ok") {
// 读取完成
resolve(true);
}
else {
reject(res.text);
}
});
});
}
/**
* 与邮箱服务器保持连接,但服务器也有可能主动关闭
* @returns
*/
async noop() {
return new Promise((resolve, reject) => {
this.sendCmd("NOOP", (res) => {
resolve(res.result === "ok");
});
});
}
/**
* 退出
* @returns
*/
async logout() {
return new Promise((resolve, reject) => {
this.sendCmd("LOGOUT", (res) => {
if (this.logined && res.result === 'ok') {
this.logined = false;
}
resolve(res.result === "ok");
});
});
}
/**
* 开始监听IDLE
* @returns
*/
async idel() {
return new Promise((resolve, reject) => {
this.sendCmd("IDLE", (res) => {
resolve(res.result === "ok");
});
});
}
/**
* 邮箱是否支持IDEL,IDEL可以实时获取邮箱状态
* @returns
*/
hasIdel() {
return this.caps.includes("IDLE");
}
isLogin() {
return this.logined;
}
async startTTLS() {
return new Promise((resolve, reject) => {
this.sendCmd("STARTTLS", (res) => {
if (res.result === "ok") {
resolve(true);
}
else {
reject(res.text);
}
});
});
}
async capability() {
return new Promise((resolve, reject) => {
this.sendCmd("CAPABILITY", (res) => {
if (res.result === "ok") {
resolve(this.caps);
}
else {
reject(res.text);
}
});
});
}
async createTLS(socket) {
const { tlsOptions, host, tlsPort } = this.options;
return new Promise((resolve, reject) => {
this.socket = tls.connect({
...tlsOptions,
host,
servername: host,
port: tlsPort,
socket,
}, () => {
this.setSocketEvent();
this.state = "connected";
resolve(true);
});
// this.setSocketEvent();
this.socket.once("error", (err) => {
reject(err);
});
this.socket.once("close", () => {
reject("close");
});
this.socket.once("end", () => {
reject("end");
});
});
}
// getStatus(name: string,){
// this.sendCmd(``)
// }
destroy() {
if (this.state === "disconnected")
return;
this.logined = false;
this.parser?.removeAllListeners();
this.parser = undefined;
this.state = "disconnected";
this.socket?.removeAllListeners();
this.socket?.destroy();
this.socket = undefined;
this.emit("destroy");
}
handleConnect(sock) {
this.state = "connected";
this.emit("connect", sock);
}
initParser() {
if (this.parser)
return;
const { logger, proxy } = this.options;
this.parser = new Parser_1.default(logger);
this.parser.on("tagged", (res) => {
// console.log(res);
const handler = this.tagHandlerMap.get(res.tag);
if (handler) {
handler(res);
this.tagHandlerMap.delete(res.tag);
}
this.currCmd = "";
if (this.cmdQueue.length > 0) {
const c = this.cmdQueue.shift();
this.sendCmd(c.cmd, c.callback);
}
});
this.parser.on("untagged", async (res) => {
// console.log("untagged: ", res);
const { type, text, num } = res;
if (type === "ok" && !this.currCmd) {
// 连接成功服务器返回欢迎信息
// 获取服务器功能
await this.capability().catch((text) => { throw new Error(text); });
if (!proxy && this.caps.includes("STARTTLS")) {
// 服务器开启STARTTLS,告诉服务器开始建立tls连接
await this.startTTLS().catch((text) => { throw new Error(text); });
// 建立tls连接
this.createTLS(this.socket).then(() => {
this.emit("ready");
// 自动登录
if (this.options.autoLogin || this.options.autoLogin === undefined)
this.login();
});
}
else {
this.emit("ready");
// 自动登录
if (this.options.autoLogin || this.options.autoLogin === undefined)
this.login();
}
}
else if (type === "exists") {
// 邮箱总数
const prev = this.box.messages.total, now = res.num || 0;
this.box.messages.total = now;
if (now > prev && this.logined) {
this.box.messages.new = now - prev;
}
this.emit("exists", this.box.messages);
if (this.idling) {
this.socket?.write("DONE\r\n");
this.idling = false;
}
}
else if (type === 'capability') {
this.caps = text.map(function (v) { return v.toUpperCase(); });
}
else if (type === "flags") {
// this.box.flags = text;
}
else if (type === "recent") {
this.box.messages.new = num || 0;
}
else if (type === "expunge") {
// 删除
// if (this.box.messages.total > 0) --this.box.messages.total;
this.emit("expunge", num || 0);
}
else if (type === "bad" || type === "no") {
if (this.state === "connected") {
const err = new Error(this.currCmd ? "Bad command " + this.currCmd : "Received negative welcome: " + text);
// console.log(err)
this.emit("cmdError", err);
this.currCmd = "";
}
}
else if (type === "search") {
this.searchUids = text;
}
else if (type === "bye") {
this.emit("bye", res);
}
});
this.parser.on("body", (data) => {
(0, mailparser_1.simpleParser)(data.contents, { skipImageLinks: true, skipTextToHtml: true }, (err, mail) => {
this.emit("mail", err, { uid: Number(data.uid), mail });
});
// this.emit("mail", null, {uid: Number(data.uid)})
});
this.parser.on("continue", (res) => {
if (res.text === "idling") {
this.idling = true;
}
});
}
setSocketEvent() {
if (!this.socket) {
throw new Error("Socket is null");
}
this.socket.removeAllListeners();
if (this.options.keepalive) {
this.socket.setKeepAlive(true);
}
const { logger } = this.options;
// this.socket.on("drain", () => {
// const cmds = this.cmdQueue;
// this.cmdQueue = [];
// cmds.forEach((c) => {
// this.sendCmd(c.cmd, c.callback);
// });
// });
this.socket.once("close", (had_err) => {
logger && logger("socket close had_err ", had_err);
this.destroy();
this.emit("close", had_err);
});
this.socket.once("end", () => {
logger && logger("socket end");
this.destroy();
this.emit("end");
});
this.socket.once("error", (err) => {
logger && logger("socket error", err);
this.destroy();
this.emit("socketError", err);
});
this.socket.once("timeout", () => {
logger && logger("socket timeout");
this.destroy();
this.emit("timeout");
});
this.socket.on("data", (data) => {
logger && logger(`<=`, data.toString("utf-8"));
this.parser?.parse(data);
});
}
}
exports.MgImap = MgImap;
//# sourceMappingURL=MgImap.js.map