@bobfrankston/iflow 1.0.34 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/imaplib/bridge-transport.d.ts +30 -0
- package/imaplib/bridge-transport.js +60 -0
- package/imaplib/imap-compat.d.ts +77 -0
- package/imaplib/imap-compat.js +238 -0
- package/imaplib/imap-native.d.ts +123 -0
- package/imaplib/imap-native.js +586 -0
- package/imaplib/imap-protocol.d.ts +131 -0
- package/imaplib/imap-protocol.js +370 -0
- package/imaplib/node-transport.d.ts +25 -0
- package/imaplib/node-transport.js +109 -0
- package/imaplib/transport.d.ts +25 -0
- package/imaplib/transport.js +6 -0
- package/index.d.ts +6 -0
- package/index.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native IMAP client — transport-agnostic.
|
|
3
|
+
* Uses ImapTransport for I/O, imap-protocol for parsing.
|
|
4
|
+
* Works with NodeTransport (desktop) or BridgeTransport (Android).
|
|
5
|
+
*
|
|
6
|
+
* This is a NEW client alongside the existing ImapClient (which wraps imapflow).
|
|
7
|
+
* Existing callers are not affected.
|
|
8
|
+
*/
|
|
9
|
+
import * as proto from "./imap-protocol.js";
|
|
10
|
+
export class NativeImapClient {
|
|
11
|
+
transport;
|
|
12
|
+
transportFactory;
|
|
13
|
+
config;
|
|
14
|
+
buffer = "";
|
|
15
|
+
pendingCommand = null;
|
|
16
|
+
capabilities = new Set();
|
|
17
|
+
_connected = false;
|
|
18
|
+
idleTag = null;
|
|
19
|
+
idleCallback = null;
|
|
20
|
+
verbose;
|
|
21
|
+
selectedMailbox = null;
|
|
22
|
+
mailboxInfo = { exists: 0, recent: 0, uidNext: 0, uidValidity: 0, flags: [], permanentFlags: [] };
|
|
23
|
+
constructor(config, transportFactory) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.transportFactory = transportFactory;
|
|
26
|
+
this.transport = transportFactory();
|
|
27
|
+
this.verbose = config.verbose || false;
|
|
28
|
+
}
|
|
29
|
+
get connected() { return this._connected; }
|
|
30
|
+
// ── Connection ──
|
|
31
|
+
async connect() {
|
|
32
|
+
const useTls = this.config.port === 993;
|
|
33
|
+
this.transport.onData((data) => this.handleData(data));
|
|
34
|
+
this.transport.onClose(() => { this._connected = false; });
|
|
35
|
+
this.transport.onError((err) => {
|
|
36
|
+
if (this.verbose)
|
|
37
|
+
console.error(` [imap] Transport error: ${err.message}`);
|
|
38
|
+
});
|
|
39
|
+
await this.transport.connect(this.config.server, this.config.port, useTls, this.config.server);
|
|
40
|
+
// Read server greeting
|
|
41
|
+
const greeting = await this.readGreeting();
|
|
42
|
+
if (this.verbose)
|
|
43
|
+
console.log(` [imap] Greeting: ${greeting.raw}`);
|
|
44
|
+
// Parse capabilities from greeting
|
|
45
|
+
if (greeting.text.includes("CAPABILITY")) {
|
|
46
|
+
this.parseCapabilities(greeting.text);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
await this.capability();
|
|
50
|
+
}
|
|
51
|
+
// STARTTLS if needed
|
|
52
|
+
if (!useTls && this.capabilities.has("STARTTLS")) {
|
|
53
|
+
await this.starttls();
|
|
54
|
+
}
|
|
55
|
+
this._connected = true;
|
|
56
|
+
// Authenticate
|
|
57
|
+
await this.authenticate();
|
|
58
|
+
}
|
|
59
|
+
async readGreeting() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const check = () => {
|
|
62
|
+
const lineEnd = this.buffer.indexOf("\r\n");
|
|
63
|
+
if (lineEnd >= 0) {
|
|
64
|
+
const line = this.buffer.substring(0, lineEnd + 2);
|
|
65
|
+
this.buffer = this.buffer.substring(lineEnd + 2);
|
|
66
|
+
resolve(proto.parseResponseLine(line));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
setTimeout(check, 10);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
check();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async authenticate() {
|
|
76
|
+
if (this.config.tokenProvider) {
|
|
77
|
+
const token = await this.config.tokenProvider();
|
|
78
|
+
const tag = proto.nextTag();
|
|
79
|
+
const cmd = proto.xoauth2Command(tag, this.config.username, token);
|
|
80
|
+
if (this.verbose)
|
|
81
|
+
console.log(` [imap] > AUTHENTICATE XOAUTH2 ...`);
|
|
82
|
+
const responses = await this.sendCommand(tag, cmd);
|
|
83
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
84
|
+
if (!tagged || tagged.type !== "OK") {
|
|
85
|
+
const errText = tagged?.text || responses.map(r => r.raw).join("; ");
|
|
86
|
+
throw new Error(`Authentication failed: ${errText}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (this.config.password) {
|
|
90
|
+
const tag = proto.nextTag();
|
|
91
|
+
const cmd = proto.loginCommand(tag, this.config.username, this.config.password);
|
|
92
|
+
if (this.verbose)
|
|
93
|
+
console.log(` [imap] > LOGIN ${this.config.username} ***`);
|
|
94
|
+
const responses = await this.sendCommand(tag, cmd);
|
|
95
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
96
|
+
if (!tagged || tagged.type !== "OK") {
|
|
97
|
+
const errText = tagged?.text || responses.map(r => r.raw).join("; ");
|
|
98
|
+
throw new Error(`Login failed: ${errText}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
throw new Error("No password or token provider configured");
|
|
103
|
+
}
|
|
104
|
+
// Re-read capabilities after auth (they may change)
|
|
105
|
+
await this.capability();
|
|
106
|
+
}
|
|
107
|
+
async starttls() {
|
|
108
|
+
const tag = proto.nextTag();
|
|
109
|
+
const responses = await this.sendCommand(tag, proto.starttlsCommand(tag));
|
|
110
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
111
|
+
if (!tagged || tagged.type !== "OK")
|
|
112
|
+
throw new Error("STARTTLS failed");
|
|
113
|
+
await this.transport.upgradeTLS(this.config.server);
|
|
114
|
+
await this.capability();
|
|
115
|
+
}
|
|
116
|
+
async capability() {
|
|
117
|
+
const tag = proto.nextTag();
|
|
118
|
+
const responses = await this.sendCommand(tag, proto.capabilityCommand(tag));
|
|
119
|
+
for (const r of responses) {
|
|
120
|
+
if (r.tag === "*" && r.type === "CAPABILITY") {
|
|
121
|
+
this.parseCapabilities(r.text);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return this.capabilities;
|
|
125
|
+
}
|
|
126
|
+
parseCapabilities(text) {
|
|
127
|
+
const caps = text.replace(/^CAPABILITY\s*/i, "").split(/\s+/);
|
|
128
|
+
this.capabilities.clear();
|
|
129
|
+
for (const c of caps)
|
|
130
|
+
this.capabilities.add(c.toUpperCase());
|
|
131
|
+
}
|
|
132
|
+
async logout() {
|
|
133
|
+
try {
|
|
134
|
+
if (this._connected) {
|
|
135
|
+
const tag = proto.nextTag();
|
|
136
|
+
await this.sendCommand(tag, proto.logoutCommand(tag));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* ignore */ }
|
|
140
|
+
this.transport.close();
|
|
141
|
+
this._connected = false;
|
|
142
|
+
}
|
|
143
|
+
// ── Mailbox Operations ──
|
|
144
|
+
async select(mailbox) {
|
|
145
|
+
const tag = proto.nextTag();
|
|
146
|
+
const responses = await this.sendCommand(tag, proto.selectCommand(tag, mailbox));
|
|
147
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
148
|
+
if (!tagged || tagged.type !== "OK") {
|
|
149
|
+
throw new Error(`SELECT ${mailbox} failed: ${tagged?.text || "unknown"}`);
|
|
150
|
+
}
|
|
151
|
+
// Parse mailbox info from untagged responses
|
|
152
|
+
for (const r of responses) {
|
|
153
|
+
if (r.tag !== "*")
|
|
154
|
+
continue;
|
|
155
|
+
if (r.type === "EXISTS") {
|
|
156
|
+
this.mailboxInfo.exists = parseInt(r.text);
|
|
157
|
+
}
|
|
158
|
+
else if (r.type === "RECENT") {
|
|
159
|
+
this.mailboxInfo.recent = parseInt(r.text);
|
|
160
|
+
}
|
|
161
|
+
else if (r.type === "FLAGS") {
|
|
162
|
+
this.mailboxInfo.flags = [...proto.parseFlags(r.text)];
|
|
163
|
+
}
|
|
164
|
+
else if (r.type === "OK") {
|
|
165
|
+
const uidNextMatch = r.text.match(/UIDNEXT\s+(\d+)/i);
|
|
166
|
+
if (uidNextMatch)
|
|
167
|
+
this.mailboxInfo.uidNext = parseInt(uidNextMatch[1]);
|
|
168
|
+
const uidValMatch = r.text.match(/UIDVALIDITY\s+(\d+)/i);
|
|
169
|
+
if (uidValMatch)
|
|
170
|
+
this.mailboxInfo.uidValidity = parseInt(uidValMatch[1]);
|
|
171
|
+
const permMatch = r.text.match(/PERMANENTFLAGS\s+\(([^)]*)\)/i);
|
|
172
|
+
if (permMatch)
|
|
173
|
+
this.mailboxInfo.permanentFlags = permMatch[1].split(/\s+/).filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.selectedMailbox = mailbox;
|
|
177
|
+
return { ...this.mailboxInfo };
|
|
178
|
+
}
|
|
179
|
+
async examine(mailbox) {
|
|
180
|
+
const tag = proto.nextTag();
|
|
181
|
+
const responses = await this.sendCommand(tag, proto.examineCommand(tag, mailbox));
|
|
182
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
183
|
+
if (!tagged || tagged.type !== "OK") {
|
|
184
|
+
throw new Error(`EXAMINE ${mailbox} failed: ${tagged?.text || "unknown"}`);
|
|
185
|
+
}
|
|
186
|
+
for (const r of responses) {
|
|
187
|
+
if (r.tag !== "*")
|
|
188
|
+
continue;
|
|
189
|
+
if (r.type === "EXISTS")
|
|
190
|
+
this.mailboxInfo.exists = parseInt(r.text);
|
|
191
|
+
else if (r.type === "RECENT")
|
|
192
|
+
this.mailboxInfo.recent = parseInt(r.text);
|
|
193
|
+
}
|
|
194
|
+
this.selectedMailbox = mailbox;
|
|
195
|
+
return { ...this.mailboxInfo };
|
|
196
|
+
}
|
|
197
|
+
/** Close the currently selected mailbox */
|
|
198
|
+
async closeMailbox() {
|
|
199
|
+
if (!this.selectedMailbox)
|
|
200
|
+
return;
|
|
201
|
+
const tag = proto.nextTag();
|
|
202
|
+
await this.sendCommand(tag, proto.buildCommand(tag, "CLOSE"));
|
|
203
|
+
this.selectedMailbox = null;
|
|
204
|
+
}
|
|
205
|
+
// ── Folder Operations ──
|
|
206
|
+
async listFolders() {
|
|
207
|
+
const tag = proto.nextTag();
|
|
208
|
+
const responses = await this.sendCommand(tag, proto.listCommand(tag));
|
|
209
|
+
const folders = [];
|
|
210
|
+
for (const r of responses) {
|
|
211
|
+
if (r.tag === "*" && r.type === "LIST") {
|
|
212
|
+
const parsed = proto.parseListResponse(r.text);
|
|
213
|
+
if (parsed)
|
|
214
|
+
folders.push(parsed);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return folders;
|
|
218
|
+
}
|
|
219
|
+
async getStatus(mailbox) {
|
|
220
|
+
const tag = proto.nextTag();
|
|
221
|
+
const responses = await this.sendCommand(tag, proto.statusCommand(tag, mailbox, ["MESSAGES", "UIDNEXT", "UNSEEN"]));
|
|
222
|
+
for (const r of responses) {
|
|
223
|
+
if (r.tag === "*" && r.type === "STATUS") {
|
|
224
|
+
const data = proto.parseStatusResponse(r.text);
|
|
225
|
+
if (data)
|
|
226
|
+
return data;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
async createMailbox(mailbox) {
|
|
232
|
+
const tag = proto.nextTag();
|
|
233
|
+
const responses = await this.sendCommand(tag, proto.createCommand(tag, mailbox));
|
|
234
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
235
|
+
if (!tagged || tagged.type !== "OK")
|
|
236
|
+
throw new Error(`CREATE failed: ${tagged?.text || "unknown"}`);
|
|
237
|
+
}
|
|
238
|
+
async deleteMailbox(mailbox) {
|
|
239
|
+
const tag = proto.nextTag();
|
|
240
|
+
const responses = await this.sendCommand(tag, proto.deleteMailboxCommand(tag, mailbox));
|
|
241
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
242
|
+
if (!tagged || tagged.type !== "OK")
|
|
243
|
+
throw new Error(`DELETE failed: ${tagged?.text || "unknown"}`);
|
|
244
|
+
}
|
|
245
|
+
async renameMailbox(from, to) {
|
|
246
|
+
const tag = proto.nextTag();
|
|
247
|
+
const responses = await this.sendCommand(tag, proto.renameCommand(tag, from, to));
|
|
248
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
249
|
+
if (!tagged || tagged.type !== "OK")
|
|
250
|
+
throw new Error(`RENAME failed: ${tagged?.text || "unknown"}`);
|
|
251
|
+
}
|
|
252
|
+
// ── Message Operations ──
|
|
253
|
+
/** Fetch messages by UID range */
|
|
254
|
+
async fetchMessages(range, options = {}) {
|
|
255
|
+
const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE"];
|
|
256
|
+
if (options.headers !== false)
|
|
257
|
+
items.push("BODY.PEEK[HEADER]");
|
|
258
|
+
if (options.source)
|
|
259
|
+
items.push("BODY.PEEK[]");
|
|
260
|
+
const tag = proto.nextTag();
|
|
261
|
+
const responses = await this.sendCommand(tag, proto.fetchCommand(tag, range, items));
|
|
262
|
+
return this.parseFetchResponses(responses);
|
|
263
|
+
}
|
|
264
|
+
/** Fetch messages since a UID */
|
|
265
|
+
async fetchSinceUid(sinceUid, options = {}) {
|
|
266
|
+
return this.fetchMessages(`${sinceUid + 1}:*`, options);
|
|
267
|
+
}
|
|
268
|
+
/** Fetch messages by date range */
|
|
269
|
+
async fetchByDate(since, before, options = {}) {
|
|
270
|
+
// First search for UIDs in date range, then fetch
|
|
271
|
+
const criteria = proto.buildSearchCriteria({
|
|
272
|
+
since,
|
|
273
|
+
before: before || undefined,
|
|
274
|
+
});
|
|
275
|
+
const uids = await this.search(criteria);
|
|
276
|
+
if (uids.length === 0)
|
|
277
|
+
return [];
|
|
278
|
+
// Fetch in chunks to avoid very long command lines
|
|
279
|
+
const chunkSize = 500;
|
|
280
|
+
const allMessages = [];
|
|
281
|
+
for (let i = 0; i < uids.length; i += chunkSize) {
|
|
282
|
+
const chunk = uids.slice(i, i + chunkSize);
|
|
283
|
+
const range = chunk.join(",");
|
|
284
|
+
const msgs = await this.fetchMessages(range, options);
|
|
285
|
+
allMessages.push(...msgs);
|
|
286
|
+
}
|
|
287
|
+
return allMessages;
|
|
288
|
+
}
|
|
289
|
+
/** Fetch a single message by UID */
|
|
290
|
+
async fetchMessage(uid, options = {}) {
|
|
291
|
+
const msgs = await this.fetchMessages(String(uid), options);
|
|
292
|
+
return msgs.find(m => m.uid === uid) || null;
|
|
293
|
+
}
|
|
294
|
+
/** Get all UIDs in the current mailbox */
|
|
295
|
+
async getUids() {
|
|
296
|
+
return this.search("ALL");
|
|
297
|
+
}
|
|
298
|
+
/** UID SEARCH */
|
|
299
|
+
async search(criteria) {
|
|
300
|
+
const tag = proto.nextTag();
|
|
301
|
+
const responses = await this.sendCommand(tag, proto.searchCommand(tag, criteria));
|
|
302
|
+
for (const r of responses) {
|
|
303
|
+
if (r.tag === "*" && r.type === "SEARCH") {
|
|
304
|
+
return proto.parseSearchResponse(r.text);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
/** Set flags on a message */
|
|
310
|
+
async addFlags(uid, flags) {
|
|
311
|
+
const tag = proto.nextTag();
|
|
312
|
+
const responses = await this.sendCommand(tag, proto.storeCommand(tag, uid, "+FLAGS.SILENT", flags));
|
|
313
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
314
|
+
if (!tagged || tagged.type !== "OK")
|
|
315
|
+
throw new Error(`STORE +FLAGS failed: ${tagged?.text || "unknown"}`);
|
|
316
|
+
}
|
|
317
|
+
/** Remove flags from a message */
|
|
318
|
+
async removeFlags(uid, flags) {
|
|
319
|
+
const tag = proto.nextTag();
|
|
320
|
+
const responses = await this.sendCommand(tag, proto.storeCommand(tag, uid, "-FLAGS.SILENT", flags));
|
|
321
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
322
|
+
if (!tagged || tagged.type !== "OK")
|
|
323
|
+
throw new Error(`STORE -FLAGS failed: ${tagged?.text || "unknown"}`);
|
|
324
|
+
}
|
|
325
|
+
/** Copy a message to another mailbox */
|
|
326
|
+
async copyMessage(uid, destination) {
|
|
327
|
+
const tag = proto.nextTag();
|
|
328
|
+
const responses = await this.sendCommand(tag, proto.copyCommand(tag, uid, destination));
|
|
329
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
330
|
+
if (!tagged || tagged.type !== "OK")
|
|
331
|
+
throw new Error(`COPY failed: ${tagged?.text || "unknown"}`);
|
|
332
|
+
}
|
|
333
|
+
/** Move a message to another mailbox (MOVE or COPY+DELETE) */
|
|
334
|
+
async moveMessage(uid, destination) {
|
|
335
|
+
if (this.capabilities.has("MOVE")) {
|
|
336
|
+
const tag = proto.nextTag();
|
|
337
|
+
const responses = await this.sendCommand(tag, proto.moveCommand(tag, uid, destination));
|
|
338
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
339
|
+
if (!tagged || tagged.type !== "OK")
|
|
340
|
+
throw new Error(`MOVE failed: ${tagged?.text || "unknown"}`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
await this.copyMessage(uid, destination);
|
|
344
|
+
await this.addFlags(uid, ["\\Deleted"]);
|
|
345
|
+
await this.expunge();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Delete a message by UID (flag + expunge) */
|
|
349
|
+
async deleteMessage(uid) {
|
|
350
|
+
await this.addFlags(uid, ["\\Deleted"]);
|
|
351
|
+
await this.expunge();
|
|
352
|
+
}
|
|
353
|
+
/** Expunge deleted messages */
|
|
354
|
+
async expunge() {
|
|
355
|
+
const tag = proto.nextTag();
|
|
356
|
+
await this.sendCommand(tag, proto.buildCommand(tag, "EXPUNGE"));
|
|
357
|
+
}
|
|
358
|
+
/** Append a message to a mailbox */
|
|
359
|
+
async appendMessage(mailbox, message, flags = []) {
|
|
360
|
+
const data = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
361
|
+
const size = new TextEncoder().encode(data).length;
|
|
362
|
+
const tag = proto.nextTag();
|
|
363
|
+
const cmd = proto.appendCommand(tag, mailbox, flags, size);
|
|
364
|
+
// Send command, wait for continuation
|
|
365
|
+
await this.transport.write(cmd);
|
|
366
|
+
// Wait for "+" continuation
|
|
367
|
+
const contResp = await this.waitForContinuation(tag);
|
|
368
|
+
if (!contResp)
|
|
369
|
+
throw new Error("APPEND: server did not send continuation");
|
|
370
|
+
// Send the message data + CRLF
|
|
371
|
+
await this.transport.write(data + "\r\n");
|
|
372
|
+
// Wait for tagged response
|
|
373
|
+
const responses = await this.waitForTagged(tag);
|
|
374
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
375
|
+
if (!tagged || tagged.type !== "OK")
|
|
376
|
+
throw new Error(`APPEND failed: ${tagged?.text || "unknown"}`);
|
|
377
|
+
// Try to extract APPENDUID
|
|
378
|
+
const uidMatch = tagged.text.match(/APPENDUID\s+\d+\s+(\d+)/i);
|
|
379
|
+
return uidMatch ? parseInt(uidMatch[1]) : null;
|
|
380
|
+
}
|
|
381
|
+
// ── IDLE ──
|
|
382
|
+
async startIdle(onNewMail) {
|
|
383
|
+
this.idleCallback = onNewMail;
|
|
384
|
+
const tag = proto.nextTag();
|
|
385
|
+
this.idleTag = tag;
|
|
386
|
+
await this.transport.write(proto.idleCommand(tag));
|
|
387
|
+
// Wait for "+" continuation
|
|
388
|
+
await this.waitForContinuation(tag);
|
|
389
|
+
return async () => {
|
|
390
|
+
this.idleTag = null;
|
|
391
|
+
this.idleCallback = null;
|
|
392
|
+
await this.transport.write(proto.doneCommand());
|
|
393
|
+
await this.waitForTagged(tag);
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// ── Message count (lightweight STATUS) ──
|
|
397
|
+
async getMessageCount(mailbox) {
|
|
398
|
+
const status = await this.getStatus(mailbox);
|
|
399
|
+
return status.messages || 0;
|
|
400
|
+
}
|
|
401
|
+
// ── Low-level command handling ──
|
|
402
|
+
sendCommand(tag, command) {
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
|
|
405
|
+
console.log(` [imap] > ${command.trimEnd()}`);
|
|
406
|
+
}
|
|
407
|
+
this.pendingCommand = { tag, resolve, reject, responses: [] };
|
|
408
|
+
this.transport.write(command).catch(reject);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
waitForContinuation(tag) {
|
|
412
|
+
return new Promise((resolve) => {
|
|
413
|
+
const timeout = setTimeout(() => resolve(null), 30000);
|
|
414
|
+
const check = () => {
|
|
415
|
+
const lineEnd = this.buffer.indexOf("\r\n");
|
|
416
|
+
if (lineEnd >= 0) {
|
|
417
|
+
const line = this.buffer.substring(0, lineEnd + 2);
|
|
418
|
+
const resp = proto.parseResponseLine(line);
|
|
419
|
+
if (resp.tag === "+") {
|
|
420
|
+
this.buffer = this.buffer.substring(lineEnd + 2);
|
|
421
|
+
clearTimeout(timeout);
|
|
422
|
+
resolve(resp);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Not continuation — process and keep waiting
|
|
426
|
+
this.buffer = this.buffer.substring(lineEnd + 2);
|
|
427
|
+
this.handleUntaggedResponse(resp);
|
|
428
|
+
}
|
|
429
|
+
if (this.transport.connected)
|
|
430
|
+
setTimeout(check, 5);
|
|
431
|
+
else {
|
|
432
|
+
clearTimeout(timeout);
|
|
433
|
+
resolve(null);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
check();
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
waitForTagged(tag) {
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
this.pendingCommand = { tag, resolve, reject, responses: [] };
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
handleData(data) {
|
|
445
|
+
this.buffer += data;
|
|
446
|
+
this.processBuffer();
|
|
447
|
+
}
|
|
448
|
+
processBuffer() {
|
|
449
|
+
while (true) {
|
|
450
|
+
// Check for literal {size}\r\n
|
|
451
|
+
if (this.pendingCommand?.literalBytes != null) {
|
|
452
|
+
if (this.buffer.length >= this.pendingCommand.literalBytes) {
|
|
453
|
+
const literal = this.buffer.substring(0, this.pendingCommand.literalBytes);
|
|
454
|
+
this.buffer = this.buffer.substring(this.pendingCommand.literalBytes);
|
|
455
|
+
this.pendingCommand.literalBuffer = (this.pendingCommand.literalBuffer || "") + literal;
|
|
456
|
+
this.pendingCommand.literalBytes = undefined;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
break; // Wait for more data
|
|
460
|
+
}
|
|
461
|
+
const lineEnd = this.buffer.indexOf("\r\n");
|
|
462
|
+
if (lineEnd < 0)
|
|
463
|
+
break;
|
|
464
|
+
const line = this.buffer.substring(0, lineEnd + 2);
|
|
465
|
+
this.buffer = this.buffer.substring(lineEnd + 2);
|
|
466
|
+
// Check for literal announcement {size}
|
|
467
|
+
const literalMatch = line.match(/\{(\d+)\}\r\n$/);
|
|
468
|
+
if (literalMatch && this.pendingCommand) {
|
|
469
|
+
this.pendingCommand.literalBytes = parseInt(literalMatch[1]);
|
|
470
|
+
// Store the line prefix before the literal
|
|
471
|
+
const linePrefix = line.substring(0, line.indexOf(`{${literalMatch[1]}}`));
|
|
472
|
+
this.pendingCommand.literalBuffer = linePrefix;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
// If we have buffered literal data, prepend it
|
|
476
|
+
let fullLine = line;
|
|
477
|
+
if (this.pendingCommand?.literalBuffer) {
|
|
478
|
+
fullLine = this.pendingCommand.literalBuffer + line;
|
|
479
|
+
this.pendingCommand.literalBuffer = undefined;
|
|
480
|
+
}
|
|
481
|
+
const resp = proto.parseResponseLine(fullLine);
|
|
482
|
+
if (this.verbose && resp.raw.length < 200) {
|
|
483
|
+
console.log(` [imap] < ${resp.raw}`);
|
|
484
|
+
}
|
|
485
|
+
// During IDLE, handle EXISTS notifications
|
|
486
|
+
if (this.idleTag && resp.tag === "*" && resp.type === "EXISTS") {
|
|
487
|
+
const count = parseInt(resp.text);
|
|
488
|
+
if (this.idleCallback && count > this.mailboxInfo.exists) {
|
|
489
|
+
const newCount = count - this.mailboxInfo.exists;
|
|
490
|
+
this.mailboxInfo.exists = count;
|
|
491
|
+
this.idleCallback(newCount);
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
// Collect untagged responses for the pending command
|
|
496
|
+
if (resp.tag === "*" && this.pendingCommand) {
|
|
497
|
+
this.pendingCommand.responses.push(resp);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// Continuation — resolve if waiting
|
|
501
|
+
if (resp.tag === "+") {
|
|
502
|
+
// Handled by waitForContinuation
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
// Tagged response — command complete
|
|
506
|
+
if (this.pendingCommand && resp.tag === this.pendingCommand.tag) {
|
|
507
|
+
this.pendingCommand.responses.push(resp);
|
|
508
|
+
const { resolve, responses } = this.pendingCommand;
|
|
509
|
+
this.pendingCommand = null;
|
|
510
|
+
resolve(responses);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
// Unhandled
|
|
514
|
+
this.handleUntaggedResponse(resp);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
handleUntaggedResponse(resp) {
|
|
518
|
+
if (resp.type === "EXISTS") {
|
|
519
|
+
this.mailboxInfo.exists = parseInt(resp.text);
|
|
520
|
+
}
|
|
521
|
+
else if (resp.type === "EXPUNGE") {
|
|
522
|
+
this.mailboxInfo.exists = Math.max(0, this.mailboxInfo.exists - 1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ── FETCH Response Parser ──
|
|
526
|
+
parseFetchResponses(responses) {
|
|
527
|
+
const messages = [];
|
|
528
|
+
for (const r of responses) {
|
|
529
|
+
if (r.tag !== "*" || r.type !== "FETCH")
|
|
530
|
+
continue;
|
|
531
|
+
const msg = {
|
|
532
|
+
seq: 0, uid: 0, flags: new Set(), date: null,
|
|
533
|
+
subject: "", messageId: "", from: [], to: [], cc: [], bcc: [],
|
|
534
|
+
sender: [], replyTo: [], inReplyTo: "", size: 0,
|
|
535
|
+
source: "", headers: "", seen: false, flagged: false,
|
|
536
|
+
answered: false, draft: false,
|
|
537
|
+
};
|
|
538
|
+
// Extract sequence number from "5 FETCH (...)"
|
|
539
|
+
const seqMatch = r.text.match(/^(\d+)\s+FETCH/);
|
|
540
|
+
if (seqMatch)
|
|
541
|
+
msg.seq = parseInt(seqMatch[1]);
|
|
542
|
+
// Extract UID
|
|
543
|
+
const uidMatch = r.text.match(/UID\s+(\d+)/);
|
|
544
|
+
if (uidMatch)
|
|
545
|
+
msg.uid = parseInt(uidMatch[1]);
|
|
546
|
+
// Extract FLAGS
|
|
547
|
+
const flagsMatch = r.text.match(/FLAGS\s+\(([^)]*)\)/);
|
|
548
|
+
if (flagsMatch) {
|
|
549
|
+
msg.flags = new Set(flagsMatch[1].split(/\s+/).filter(Boolean));
|
|
550
|
+
msg.seen = msg.flags.has("\\Seen");
|
|
551
|
+
msg.flagged = msg.flags.has("\\Flagged");
|
|
552
|
+
msg.answered = msg.flags.has("\\Answered");
|
|
553
|
+
msg.draft = msg.flags.has("\\Draft");
|
|
554
|
+
}
|
|
555
|
+
// Extract RFC822.SIZE
|
|
556
|
+
const sizeMatch = r.text.match(/RFC822\.SIZE\s+(\d+)/);
|
|
557
|
+
if (sizeMatch)
|
|
558
|
+
msg.size = parseInt(sizeMatch[1]);
|
|
559
|
+
// Extract INTERNALDATE
|
|
560
|
+
const dateMatch = r.text.match(/INTERNALDATE\s+"([^"]+)"/);
|
|
561
|
+
if (dateMatch)
|
|
562
|
+
msg.date = new Date(dateMatch[1]);
|
|
563
|
+
// Extract ENVELOPE
|
|
564
|
+
const envMatch = r.text.match(/ENVELOPE\s+(\(.*\))/);
|
|
565
|
+
if (envMatch) {
|
|
566
|
+
const env = proto.parseEnvelope(envMatch[1]);
|
|
567
|
+
msg.date = msg.date || env.date;
|
|
568
|
+
msg.subject = env.subject;
|
|
569
|
+
msg.messageId = env.messageId;
|
|
570
|
+
msg.from = env.from;
|
|
571
|
+
msg.to = env.to;
|
|
572
|
+
msg.cc = env.cc;
|
|
573
|
+
msg.bcc = env.bcc;
|
|
574
|
+
msg.sender = env.sender;
|
|
575
|
+
msg.replyTo = env.replyTo;
|
|
576
|
+
msg.inReplyTo = env.inReplyTo;
|
|
577
|
+
}
|
|
578
|
+
// Source body is in literal data (handled by literal buffering in processBuffer)
|
|
579
|
+
// For now, simplified — the literal handling needs more work for production
|
|
580
|
+
// TODO: improve literal parsing for BODY[] and BODY[HEADER]
|
|
581
|
+
messages.push(msg);
|
|
582
|
+
}
|
|
583
|
+
return messages;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
//# sourceMappingURL=imap-native.js.map
|