@cull/imap
Version:
A simple, configurable javascript interface exposing mailboxes and messages via IMAP.
360 lines (359 loc) • 14.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const uuid_1 = require("uuid");
const events_1 = require("events");
const connection_1 = __importDefault(require("./connection"));
const response_1 = require("./response");
const code_1 = require("./code");
const command_1 = require("./command");
const mailbox_1 = require("./mailbox");
const message_1 = __importDefault(require("./message"));
/**
* __An IMAP Client__
*/
class Client extends events_1.EventEmitter {
constructor(preferences) {
var _a;
super();
/**
* Capabilities the server has communicated.
* @link https://tools.ietf.org/html/rfc3501#section-7.2.1
*/
this.capabilities = new Set();
/**
* A cache of mailboxes the server has communicated.
*/
this._mailboxes = new Map();
/**
* A cache of envelopes the server has communicated.
*/
this._envelopes = new Map();
/**
* A cache of messages the server has communicated.
*/
this._messages = new Map();
this.id = (_a = preferences.id) !== null && _a !== void 0 ? _a : uuid_1.v4();
this.connection = new connection_1.default(preferences);
this.initialize();
}
initialize() {
this.connection.on('error', error => {
this.emit('error', error);
});
this.connection.on('receive', response => {
this.analyzeResponse(response);
});
}
connect(login = true) {
return __awaiter(this, void 0, void 0, function* () {
let response = yield this.connection.connect(login);
if (response.status === response_1.Status.OK) {
return Promise.resolve(true);
}
return Promise.reject(response);
});
}
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
let response = yield this.connection.disconnect();
if (response.status === response_1.Status.OK) {
this.selected = undefined;
this.capabilities = new Set();
return Promise.resolve(true);
}
return Promise.reject(response);
});
}
/**
* Analyze a response and update connection data as applicable.
*/
analyzeResponse(response) {
// Response Codes
response.codes.forEach(c => {
switch (c.code) {
case code_1.Code.CAPABILITY:
c.data.forEach(capability => this.capabilities.add(capability));
break;
case code_1.Code.PERMANENTFLAGS:
if (this.selected) {
c.data.forEach(flag => { var _a; return (_a = this.selected) === null || _a === void 0 ? void 0 : _a.flags.set(flag, true); });
}
break;
case code_1.Code.READONLY:
if (this.selected) {
this.selected.writeable = false;
}
break;
case code_1.Code.READWRITE:
if (this.selected) {
this.selected.writeable = true;
}
break;
case code_1.Code.UIDNEXT:
if (this.selected) {
this.selected.uid.next = c.data;
}
break;
case code_1.Code.UIDVALIDITY:
if (this.selected) {
this.selected.uid.validity = c.data;
}
break;
default:
this.emit('debug', ['unhandled response code', c, response]);
break;
}
});
// Response Data
Object.keys(response.data).forEach(key => {
var _a, _b;
switch (key) {
case response_1.ServerStatus.CAPABILITY:
response.data[key].forEach(capability => this.capabilities.add(capability));
break;
case response_1.ServerStatus.LIST:
response.data[key].forEach(mailbox => this._mailboxes.set(mailbox.name, mailbox));
break;
case response_1.ServerStatus.FLAGS:
if (this.selected) {
response.data[key].forEach(flag => { var _a; return (_a = this.selected) === null || _a === void 0 ? void 0 : _a.flags.set(flag, false); });
}
break;
case response_1.MailboxSizeUpdate.EXISTS:
if (this.selected) {
this.selected.exists = (_a = response.data[key]) !== null && _a !== void 0 ? _a : 0;
}
break;
case response_1.MailboxSizeUpdate.RECENT:
if (this.selected) {
this.selected.recent = (_b = response.data[key]) !== null && _b !== void 0 ? _b : 0;
}
break;
case response_1.MessageStatus.FETCH:
let data = response.data[key];
if (data !== undefined) {
this.analyzeFetchResponseData(data);
}
break;
default:
this.emit('debug', ['unhandled response data', key, response]);
break;
}
});
}
analyzeFetchResponseData(data) {
data.forEach((datum, sequence) => {
let message = new message_1.default();
Object.keys(datum).forEach(item => {
switch (item) {
case response_1.MessageDataItem.ENVELOPE:
message.envelope = datum[item];
break;
case response_1.MessageDataItem.UID:
message.uid = datum[item];
break;
case response_1.MessageDataItem.FLAGS:
message.flags = datum[item];
break;
case response_1.MessageDataItem.BODY:
message.body = datum[item];
break;
default:
this.emit('debug', `Unhandled message data item: ${item}`);
break;
}
});
if (message.envelope !== undefined) {
this._envelopes.set(sequence, message.envelope);
}
this._messages.set(sequence, message);
});
}
/**
* Get Mailboxes
* @param path (`string`, default: empty) The name of a mailbox or level of hierarchy
* @param children (`boolean`, default: `true`) Return children under this hierarchy
* @param flatten (`boolean`, default: `false`) Return a flat array vs a nested array (tree)
* @link https://tools.ietf.org/html/rfc3501#section-6.3.8
*/
mailboxes(path = '', children = true, flatten = false) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
try {
let wildcard = children ? '*' : '%';
let command = new command_1.Command('list', `"${path}" ${wildcard}`);
let response = yield this.connection.exchange(command);
if (response.status === response_1.Status.OK) {
let mailboxes = flatten ? this._mailboxes : exports.mailboxTree(this._mailboxes);
return resolve(mailboxes);
}
throw response;
}
catch (error) {
return reject(error);
}
}));
});
}
/**
* Get Mailbox
* @link https://tools.ietf.org/html/rfc3501#section-6.3.8
* @param name (`string`) The name of a mailbox or level of hierarchy
* @param path (`string`, default: `/`)
* @param stale (`boolean`, default: `true`) allow possibly stale (cached) result
*/
mailbox(name, path = '/', stale = true) {
return __awaiter(this, void 0, void 0, function* () {
let mailbox;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (stale) {
mailbox = this._mailboxes.get(path);
if (mailbox !== undefined)
return resolve(mailbox);
}
try {
let command = new command_1.Command('list', `"${path}" "${name}"`);
let response = yield this.connection.exchange(command);
if (response.status === response_1.Status.OK) {
mailbox = this._mailboxes.get(name);
return mailbox !== undefined
? resolve(mailbox)
: reject(new Error('Mailbox could not be found.'));
}
throw response;
}
catch (error) {
return reject(error);
}
}));
});
}
/**
* Select a Mailbox for subsequent command context.
* @link https://tools.ietf.org/html/rfc3501#section-6.3.1
* @param name (`string`)
*/
select(name) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (this.selected !== undefined && this.selected.name === name) {
return resolve(this.selected);
}
try {
this.selected = new mailbox_1.SelectedMailbox(name);
let select = new command_1.Command('select', name);
let response = yield this.connection.exchange(select);
if (response.status === response_1.Status.OK) {
return resolve(this.selected);
}
throw response;
}
catch (error) {
this.selected = undefined;
return reject(error);
}
}));
});
}
/**
* Fetch Envelopes for a given mailbox and sequence
* @link https://tools.ietf.org/html/rfc3501#section-6.4.5
* @param name (`string`) The mailbox name/path
* @param sequence (`string) sequence set
* @param timeout (`number`) timeout in seconds
*/
envelopes(name = 'INBOX', sequence = '1:10', timeout) {
return __awaiter(this, void 0, void 0, function* () {
this._envelopes = new Map();
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
try {
yield this.select(name);
let command = new command_1.Command('fetch', `${sequence} envelope`);
let response = yield this.connection.exchange(command, timeout);
if (response.status === response_1.Status.OK) {
return resolve(this._envelopes);
}
throw response;
}
catch (error) {
return reject(error);
}
}));
});
}
messages(name = 'INBOX', sequence = '1:10', items = ['UID', 'FLAGS', 'BODY.PEEK[]'], timeout) {
return __awaiter(this, void 0, void 0, function* () {
this._messages = new Map();
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
try {
yield this.select(name);
let command = new command_1.Command('fetch', `${sequence} (${items.join(' ')})`);
let response = yield this.connection.exchange(command, timeout);
if (response.status === response_1.Status.OK) {
return resolve(this._messages);
}
throw response;
}
catch (error) {
return reject(error);
}
}));
});
}
headers(name = 'INBOX', sequence = '1:10', timeout) {
return __awaiter(this, void 0, void 0, function* () {
try {
let headers = new Map();
let messages = yield this.messages(name, sequence, ['UID', 'FLAGS', 'BODY.PEEK[HEADER]'], timeout);
messages.forEach((message, key) => {
if (message.body !== undefined && message.body.HEADER !== undefined) {
headers.set(key, message.body.HEADER);
}
});
return Promise.resolve(headers);
}
catch (error) {
return Promise.reject(error);
}
});
}
}
exports.Client = Client;
exports.default = Client;
exports.mailboxTree = (map) => {
let mailboxes = [...map.values()];
mailboxes.forEach(mailbox => {
// reset for idempotency
delete mailbox.children;
});
let tree = new Map();
mailboxes.forEach(mailbox => {
let components = mailbox.name.split(mailbox.delimiter);
if (components.length > 1) {
components.pop();
let parent = map.get(components.join(mailbox.delimiter));
if (parent) {
parent.children ? parent.children.push(mailbox) : (parent.children = [mailbox]);
}
else {
throw new Error(`Dangling mailbox: ${mailbox}`);
}
}
else {
tree.set(mailbox.name, mailbox);
}
});
return tree;
};