@bobfrankston/iflow 1.0.2 → 1.0.4
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/iflow.code-workspace +42 -0
- package/imaplib/ImapClient.d.ts +65 -1
- package/imaplib/ImapClient.js +309 -21
- package/imaplib/gmail.d.ts +43 -0
- package/imaplib/gmail.js +103 -0
- package/imaplib/types.d.ts +4 -1
- package/index.d.ts +7 -0
- package/index.js +9 -0
- package/package.json +31 -9
- package/.claude/settings.local.json +0 -10
- package/imaplib/ImapClient.d.ts.map +0 -1
- package/imaplib/ImapClient.js.map +0 -1
- package/imaplib/ImapClient.ts +0 -376
- package/imaplib/types.d.ts.map +0 -1
- package/imaplib/types.js.map +0 -1
- package/imaplib/types.ts +0 -105
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{
|
|
4
|
+
"path": "."
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"path": "../imail"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "../uinfo"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "../mlconfig"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "../config"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"settings": {
|
|
20
|
+
"workbench.colorCustomizations": {
|
|
21
|
+
"activityBar.activeBackground": "#ebe835",
|
|
22
|
+
"activityBar.background": "#ebe835",
|
|
23
|
+
"activityBar.foreground": "#15202b",
|
|
24
|
+
"activityBar.inactiveForeground": "#15202b99",
|
|
25
|
+
"activityBarBadge.background": "#11aeab",
|
|
26
|
+
"activityBarBadge.foreground": "#e7e7e7",
|
|
27
|
+
"commandCenter.border": "#15202b99",
|
|
28
|
+
"sash.hoverBorder": "#ebe835",
|
|
29
|
+
"statusBar.background": "#d9d515",
|
|
30
|
+
"statusBar.foreground": "#15202b",
|
|
31
|
+
"statusBarItem.hoverBackground": "#aaa710",
|
|
32
|
+
"statusBarItem.remoteBackground": "#d9d515",
|
|
33
|
+
"statusBarItem.remoteForeground": "#15202b",
|
|
34
|
+
"titleBar.activeBackground": "#d9d515",
|
|
35
|
+
"titleBar.activeForeground": "#15202b",
|
|
36
|
+
"titleBar.inactiveBackground": "#d9d51599",
|
|
37
|
+
"titleBar.inactiveForeground": "#15202b99"
|
|
38
|
+
},
|
|
39
|
+
"peacock.color": "#d9d515",
|
|
40
|
+
"workbench.colorTheme": "Visual Studio 2019 Dark"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/imaplib/ImapClient.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import imapflow from 'imapflow';
|
|
1
|
+
import imapflow, { SearchObject } from 'imapflow';
|
|
2
2
|
import { ImapClientConfig, FetchedMessage } from './types.js';
|
|
3
3
|
export interface iFetchOptions {
|
|
4
4
|
source: boolean;
|
|
@@ -8,15 +8,68 @@ export declare class ImapClient {
|
|
|
8
8
|
private config;
|
|
9
9
|
private isDestroying;
|
|
10
10
|
private connectionPromise;
|
|
11
|
+
private initialized;
|
|
12
|
+
private reconnectAttempts;
|
|
11
13
|
constructor(cfg: ImapClientConfig);
|
|
12
14
|
private init;
|
|
15
|
+
private getAuth;
|
|
13
16
|
private handleReconnection;
|
|
14
17
|
private cleanup;
|
|
15
18
|
private withConnection;
|
|
16
19
|
getMessagesCount(mailbox: string): Promise<number>;
|
|
20
|
+
/** Get all UIDs in a mailbox (lightweight — no message data fetched) */
|
|
21
|
+
getUids(mailbox: string): Promise<number[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Search messages in a mailbox using IMAP SEARCH criteria.
|
|
24
|
+
* Returns UIDs of matching messages. Use fetchMessageByUid to get full content.
|
|
25
|
+
*
|
|
26
|
+
* @param mailbox Mailbox path (e.g., "INBOX")
|
|
27
|
+
* @param criteria imapflow SearchObject — supports from, to, subject, body,
|
|
28
|
+
* before/since dates, flags, size, header, boolean (or/not), and Gmail raw search (gmraw).
|
|
29
|
+
* See: https://imapflow.com/module-imapflow-ImapFlow.html#search
|
|
30
|
+
* @returns Array of matching UIDs
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Text search in body
|
|
34
|
+
* await client.searchMessages("INBOX", { body: "meeting" });
|
|
35
|
+
* // From + date range
|
|
36
|
+
* await client.searchMessages("INBOX", { from: "bob", since: new Date("2026-01-01") });
|
|
37
|
+
* // Gmail native search
|
|
38
|
+
* await client.searchMessages("INBOX", { gmraw: "has:attachment from:alice" });
|
|
39
|
+
*/
|
|
40
|
+
searchMessages(mailbox: string, criteria: SearchObject): Promise<number[]>;
|
|
17
41
|
fetchMessageByDate(mailbox: string, start: Date | number, endingx?: Date | number, options?: iFetchOptions): Promise<FetchedMessage[]>;
|
|
18
42
|
fetchMessages(mailbox: string, end: number, count: number, options?: iFetchOptions): Promise<FetchedMessage[]>;
|
|
43
|
+
/** Fetch messages with UID greater than sinceUid */
|
|
44
|
+
fetchMessagesSinceUid(mailbox: string, sinceUid: number, options?: iFetchOptions): Promise<FetchedMessage[]>;
|
|
45
|
+
/** Fetch a single message by UID, returning source by default */
|
|
46
|
+
fetchMessageByUid(mailbox: string, uid: number, options?: iFetchOptions): Promise<FetchedMessage | null>;
|
|
19
47
|
moveMessage(msg: FetchedMessage, sourceMailbox: string, targetMailbox: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Append a raw message to a mailbox
|
|
50
|
+
* @param mailbox Target mailbox path
|
|
51
|
+
* @param rawMessage Raw RFC822 message content
|
|
52
|
+
* @param flags Optional message flags (e.g., ['\\Seen', '\\Flagged'])
|
|
53
|
+
* @param internalDate Optional internal date for the message
|
|
54
|
+
*/
|
|
55
|
+
appendMessage(mailbox: string, rawMessage: string | Buffer, flags?: string[], internalDate?: Date): Promise<false | imapflow.AppendResponseObject>;
|
|
56
|
+
/**
|
|
57
|
+
* Copy a message to another IMAP server/account
|
|
58
|
+
* @param msg Message to copy
|
|
59
|
+
* @param targetClient Target ImapClient (different server/account)
|
|
60
|
+
* @param targetMailbox Target mailbox on the destination server
|
|
61
|
+
* @param preserveFlags Whether to copy message flags (default: true)
|
|
62
|
+
*/
|
|
63
|
+
copyMessageToServer(msg: FetchedMessage, targetClient: ImapClient, targetMailbox: string, preserveFlags?: boolean): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Move a message to another IMAP server/account (copy then delete from source)
|
|
66
|
+
* @param msg Message to move
|
|
67
|
+
* @param sourceMailbox Source mailbox path
|
|
68
|
+
* @param targetClient Target ImapClient (different server/account)
|
|
69
|
+
* @param targetMailbox Target mailbox on the destination server
|
|
70
|
+
* @param preserveFlags Whether to copy message flags (default: true)
|
|
71
|
+
*/
|
|
72
|
+
moveMessageToServer(msg: FetchedMessage, sourceMailbox: string, targetClient: ImapClient, targetMailbox: string, preserveFlags?: boolean): Promise<void>;
|
|
20
73
|
getFolderList(): Promise<imapflow.ListResponse[]>;
|
|
21
74
|
getFolderTree(): Promise<imapflow.ListTreeResponse>;
|
|
22
75
|
getSpecialFolders(mailboxes: Awaited<ReturnType<ImapClient["getFolderList"]>>): {
|
|
@@ -29,6 +82,17 @@ export declare class ImapClient {
|
|
|
29
82
|
junk: string;
|
|
30
83
|
};
|
|
31
84
|
createmailbox(folder: string[] | string): Promise<imapflow.MailboxCreateResponse>;
|
|
85
|
+
/** Add flags to a message by UID */
|
|
86
|
+
addFlags(mailbox: string, uid: number, flags: string[]): Promise<boolean>;
|
|
87
|
+
/** Remove flags from a message by UID */
|
|
88
|
+
removeFlags(mailbox: string, uid: number, flags: string[]): Promise<boolean>;
|
|
89
|
+
/** Get flags for a message by UID */
|
|
90
|
+
getFlags(mailbox: string, uid: number): Promise<string[]>;
|
|
91
|
+
/** Delete a message by UID from a mailbox */
|
|
92
|
+
deleteMessageByUid(mailbox: string, uid: number): Promise<void>;
|
|
93
|
+
/** Watch a mailbox for new messages via IMAP IDLE. Calls onNew when messages arrive.
|
|
94
|
+
* Returns a stop function to end the watch. */
|
|
95
|
+
watchMailbox(mailbox: string, onNew: (count: number) => void): Promise<() => void>;
|
|
32
96
|
logout(): Promise<void>;
|
|
33
97
|
connect(): Promise<void>;
|
|
34
98
|
}
|
package/imaplib/ImapClient.js
CHANGED
|
@@ -11,57 +11,80 @@ export class ImapClient {
|
|
|
11
11
|
config;
|
|
12
12
|
isDestroying = false;
|
|
13
13
|
connectionPromise = null;
|
|
14
|
+
initialized;
|
|
15
|
+
reconnectAttempts = 0;
|
|
14
16
|
constructor(cfg) {
|
|
15
17
|
this.config = cfg;
|
|
16
|
-
|
|
18
|
+
if (!cfg.password && !cfg.tokenProvider) {
|
|
19
|
+
throw new Error('ImapClientConfig requires either password or tokenProvider');
|
|
20
|
+
}
|
|
21
|
+
this.initialized = this.init();
|
|
17
22
|
}
|
|
18
|
-
init() {
|
|
23
|
+
async init() {
|
|
24
|
+
const auth = await this.getAuth();
|
|
19
25
|
this.client = new ImapFlow({
|
|
20
26
|
host: this.config.server,
|
|
21
27
|
port: this.config.port,
|
|
22
28
|
secure: true,
|
|
23
|
-
auth
|
|
24
|
-
user: this.config.username,
|
|
25
|
-
pass: this.config.password
|
|
26
|
-
},
|
|
29
|
+
auth,
|
|
27
30
|
logger: false, // Disable internal logging
|
|
31
|
+
tls: {
|
|
32
|
+
rejectUnauthorized: this.config.rejectUnauthorized !== false // Default to true (secure), but allow override
|
|
33
|
+
}
|
|
28
34
|
});
|
|
29
35
|
// Set up error handlers
|
|
30
36
|
this.client.on('error', (err) => {
|
|
31
|
-
console.error(
|
|
37
|
+
console.error(`${new Date().toISOString()} ImapFlow socket error: ${err.message}`);
|
|
32
38
|
if (!this.isDestroying) {
|
|
33
39
|
this.handleReconnection();
|
|
34
40
|
}
|
|
35
41
|
});
|
|
36
42
|
this.client.on('close', () => {
|
|
37
|
-
console.error(
|
|
43
|
+
console.error(`${new Date().toISOString()} ImapFlow connection closed`);
|
|
38
44
|
if (!this.isDestroying) {
|
|
39
45
|
this.handleReconnection();
|
|
40
46
|
}
|
|
41
47
|
});
|
|
42
48
|
}
|
|
49
|
+
async getAuth() {
|
|
50
|
+
if (this.config.tokenProvider) {
|
|
51
|
+
const accessToken = await this.config.tokenProvider();
|
|
52
|
+
return { user: this.config.username, accessToken };
|
|
53
|
+
}
|
|
54
|
+
return { user: this.config.username, pass: this.config.password };
|
|
55
|
+
}
|
|
43
56
|
async handleReconnection() {
|
|
44
57
|
if (this.connectionPromise)
|
|
45
58
|
return; // Already reconnecting
|
|
59
|
+
this.reconnectAttempts++;
|
|
60
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // Backoff: 1s, 2s, 4s, ... 30s max
|
|
46
61
|
this.connectionPromise = new Promise(async (resolve) => {
|
|
47
62
|
try {
|
|
48
|
-
// Clean up old client
|
|
63
|
+
// Clean up old client listeners
|
|
49
64
|
this.cleanup();
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
// Force-close the underlying socket to stop repeated error events
|
|
66
|
+
try {
|
|
67
|
+
if (this.client) {
|
|
68
|
+
this.client.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { /* ignore close errors */ }
|
|
72
|
+
console.log(`${new Date().toISOString()} Reconnecting (attempt ${this.reconnectAttempts}, waiting ${delay}ms)...`);
|
|
73
|
+
await new Promise(r => setTimeout(r, delay));
|
|
74
|
+
// Reinitialize (re-fetches token if using OAuth)
|
|
75
|
+
await this.init();
|
|
54
76
|
try {
|
|
55
77
|
await this.client.connect();
|
|
56
|
-
console.log(
|
|
78
|
+
console.log(`${new Date().toISOString()} Reconnected successfully`);
|
|
79
|
+
this.reconnectAttempts = 0; // Reset on success
|
|
57
80
|
}
|
|
58
81
|
catch (err) {
|
|
59
|
-
console.error(
|
|
82
|
+
console.error(`${new Date().toISOString()} Reconnection failed: ${err.message}`);
|
|
60
83
|
// Will trigger another reconnection attempt via error handler
|
|
61
84
|
}
|
|
62
85
|
}
|
|
63
86
|
catch (err) {
|
|
64
|
-
console.error(
|
|
87
|
+
console.error(`${new Date().toISOString()} Error during reconnection process: ${err.message}`);
|
|
65
88
|
}
|
|
66
89
|
finally {
|
|
67
90
|
this.connectionPromise = null;
|
|
@@ -83,13 +106,15 @@ export class ImapClient {
|
|
|
83
106
|
return await operation();
|
|
84
107
|
}
|
|
85
108
|
catch (err) {
|
|
86
|
-
console.error(
|
|
109
|
+
console.error(`${new Date().toISOString()} Operation failed: ${err.message}`);
|
|
87
110
|
// Check if it's a connection-related error
|
|
88
111
|
if (err.code === 'ECONNRESET' || err.code === 'ENOTFOUND' ||
|
|
89
112
|
err.code === 'ETIMEDOUT' || err.code === 'ECONNREFUSED' ||
|
|
90
|
-
err.
|
|
113
|
+
err.code === 'CERT_HAS_EXPIRED' || err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
114
|
+
err.message.includes('connection') || err.message.includes('socket') ||
|
|
115
|
+
err.message.includes('certificate')) {
|
|
91
116
|
if (!this.isDestroying) {
|
|
92
|
-
console.log(
|
|
117
|
+
console.log(`${new Date().toISOString()} Connection error detected, attempting reconnection...`);
|
|
93
118
|
await this.handleReconnection();
|
|
94
119
|
// Retry the operation once after reconnection
|
|
95
120
|
try {
|
|
@@ -97,7 +122,7 @@ export class ImapClient {
|
|
|
97
122
|
return await operation();
|
|
98
123
|
}
|
|
99
124
|
catch (retryErr) {
|
|
100
|
-
console.error(
|
|
125
|
+
console.error(`${new Date().toISOString()} Operation failed after reconnection: ${retryErr}`);
|
|
101
126
|
throw retryErr;
|
|
102
127
|
}
|
|
103
128
|
}
|
|
@@ -113,6 +138,55 @@ export class ImapClient {
|
|
|
113
138
|
return status.messages;
|
|
114
139
|
});
|
|
115
140
|
}
|
|
141
|
+
/** Get all UIDs in a mailbox (lightweight — no message data fetched) */
|
|
142
|
+
async getUids(mailbox) {
|
|
143
|
+
try {
|
|
144
|
+
await this.connect();
|
|
145
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
146
|
+
const uids = await this.client.search({ all: true }, { uid: true });
|
|
147
|
+
return uids;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error(`Error getting UIDs: ${error.message}`);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await this.client.mailboxClose();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Search messages in a mailbox using IMAP SEARCH criteria.
|
|
159
|
+
* Returns UIDs of matching messages. Use fetchMessageByUid to get full content.
|
|
160
|
+
*
|
|
161
|
+
* @param mailbox Mailbox path (e.g., "INBOX")
|
|
162
|
+
* @param criteria imapflow SearchObject — supports from, to, subject, body,
|
|
163
|
+
* before/since dates, flags, size, header, boolean (or/not), and Gmail raw search (gmraw).
|
|
164
|
+
* See: https://imapflow.com/module-imapflow-ImapFlow.html#search
|
|
165
|
+
* @returns Array of matching UIDs
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // Text search in body
|
|
169
|
+
* await client.searchMessages("INBOX", { body: "meeting" });
|
|
170
|
+
* // From + date range
|
|
171
|
+
* await client.searchMessages("INBOX", { from: "bob", since: new Date("2026-01-01") });
|
|
172
|
+
* // Gmail native search
|
|
173
|
+
* await client.searchMessages("INBOX", { gmraw: "has:attachment from:alice" });
|
|
174
|
+
*/
|
|
175
|
+
async searchMessages(mailbox, criteria) {
|
|
176
|
+
try {
|
|
177
|
+
await this.connect();
|
|
178
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
179
|
+
const uids = await this.client.search(criteria, { uid: true });
|
|
180
|
+
return uids;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error(`Search error in ${mailbox}: ${error.message}`);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await this.client.mailboxClose();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
116
190
|
async fetchMessageByDate(mailbox, start, endingx, options) {
|
|
117
191
|
try {
|
|
118
192
|
options = { ...defaultFetchOptions, ...options }; // Merge with default options
|
|
@@ -183,6 +257,61 @@ export class ImapClient {
|
|
|
183
257
|
// await this.client.logout();
|
|
184
258
|
}
|
|
185
259
|
}
|
|
260
|
+
/** Fetch messages with UID greater than sinceUid */
|
|
261
|
+
async fetchMessagesSinceUid(mailbox, sinceUid, options) {
|
|
262
|
+
try {
|
|
263
|
+
options = { ...defaultFetchOptions, ...options };
|
|
264
|
+
await this.connect();
|
|
265
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
266
|
+
// UID range: sinceUid+1:* means all UIDs after sinceUid
|
|
267
|
+
const range = `${sinceUid + 1}:*`;
|
|
268
|
+
const fetchQuery = {
|
|
269
|
+
uid: true,
|
|
270
|
+
envelope: true,
|
|
271
|
+
flags: true,
|
|
272
|
+
headers: true,
|
|
273
|
+
source: options.source
|
|
274
|
+
};
|
|
275
|
+
const messageObjects = await this.client.fetchAll(range, fetchQuery, { uid: true });
|
|
276
|
+
return messageObjects.map(msg => new FetchedMessage(msg));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
// "Nothing to fetch" just means no new messages
|
|
280
|
+
if (error.responseText?.includes("Nothing to fetch"))
|
|
281
|
+
return [];
|
|
282
|
+
console.error(`Error fetching messages since UID ${sinceUid}: ${error.message}`);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
await this.client.mailboxClose();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Fetch a single message by UID, returning source by default */
|
|
290
|
+
async fetchMessageByUid(mailbox, uid, options) {
|
|
291
|
+
try {
|
|
292
|
+
options = { ...defaultFetchOptions, ...options };
|
|
293
|
+
await this.connect();
|
|
294
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
295
|
+
const fetchQuery = {
|
|
296
|
+
uid: true,
|
|
297
|
+
envelope: true,
|
|
298
|
+
flags: true,
|
|
299
|
+
headers: true,
|
|
300
|
+
source: options.source
|
|
301
|
+
};
|
|
302
|
+
const msg = await this.client.fetchOne(uid, fetchQuery, { uid: true });
|
|
303
|
+
if (!msg)
|
|
304
|
+
return null;
|
|
305
|
+
return new FetchedMessage(msg);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error(`Error fetching message UID ${uid}: ${error.message}`);
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await this.client.mailboxClose();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
186
315
|
// async moveMessage(uid: number, sourceMailbox: string, targetMailbox: string) {
|
|
187
316
|
async moveMessage(msg, sourceMailbox, targetMailbox) {
|
|
188
317
|
try {
|
|
@@ -208,6 +337,82 @@ export class ImapClient {
|
|
|
208
337
|
await this.client.mailboxClose();
|
|
209
338
|
}
|
|
210
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Append a raw message to a mailbox
|
|
342
|
+
* @param mailbox Target mailbox path
|
|
343
|
+
* @param rawMessage Raw RFC822 message content
|
|
344
|
+
* @param flags Optional message flags (e.g., ['\\Seen', '\\Flagged'])
|
|
345
|
+
* @param internalDate Optional internal date for the message
|
|
346
|
+
*/
|
|
347
|
+
async appendMessage(mailbox, rawMessage, flags, internalDate) {
|
|
348
|
+
try {
|
|
349
|
+
await this.connect();
|
|
350
|
+
const result = await this.client.append(mailbox, rawMessage, flags, internalDate);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
console.error(`Error appending message to ${mailbox}: ${error.message}`);
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Copy a message to another IMAP server/account
|
|
360
|
+
* @param msg Message to copy
|
|
361
|
+
* @param targetClient Target ImapClient (different server/account)
|
|
362
|
+
* @param targetMailbox Target mailbox on the destination server
|
|
363
|
+
* @param preserveFlags Whether to copy message flags (default: true)
|
|
364
|
+
*/
|
|
365
|
+
async copyMessageToServer(msg, targetClient, targetMailbox, preserveFlags = true) {
|
|
366
|
+
try {
|
|
367
|
+
if (!msg.source) {
|
|
368
|
+
throw new Error('Message source is required for cross-server copy. Fetch with source: true');
|
|
369
|
+
}
|
|
370
|
+
// Prepare flags if preserving them
|
|
371
|
+
const flags = [];
|
|
372
|
+
if (preserveFlags) {
|
|
373
|
+
if (msg.flagged)
|
|
374
|
+
flags.push('\\Flagged');
|
|
375
|
+
if (msg.seen)
|
|
376
|
+
flags.push('\\Seen');
|
|
377
|
+
if (msg.answered)
|
|
378
|
+
flags.push('\\Answered');
|
|
379
|
+
if (msg.draft)
|
|
380
|
+
flags.push('\\Draft');
|
|
381
|
+
}
|
|
382
|
+
// Append to target server
|
|
383
|
+
const date = msg.internalDate instanceof Date ? msg.internalDate : undefined;
|
|
384
|
+
await targetClient.appendMessage(targetMailbox, msg.source, flags, date);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
console.error(`Error copying message to another server: ${error.message}`);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Move a message to another IMAP server/account (copy then delete from source)
|
|
393
|
+
* @param msg Message to move
|
|
394
|
+
* @param sourceMailbox Source mailbox path
|
|
395
|
+
* @param targetClient Target ImapClient (different server/account)
|
|
396
|
+
* @param targetMailbox Target mailbox on the destination server
|
|
397
|
+
* @param preserveFlags Whether to copy message flags (default: true)
|
|
398
|
+
*/
|
|
399
|
+
async moveMessageToServer(msg, sourceMailbox, targetClient, targetMailbox, preserveFlags = true) {
|
|
400
|
+
try {
|
|
401
|
+
// First copy to target
|
|
402
|
+
await this.copyMessageToServer(msg, targetClient, targetMailbox, preserveFlags);
|
|
403
|
+
// Then delete from source
|
|
404
|
+
await this.connect();
|
|
405
|
+
await this.client.mailboxOpen(sourceMailbox, { readOnly: false });
|
|
406
|
+
await this.client.messageDelete([msg.uid], { uid: true });
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
console.error(`Error moving message to another server: ${error.message}`);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
await this.client.mailboxClose();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
211
416
|
async getFolderList() {
|
|
212
417
|
return this.withConnection(async () => {
|
|
213
418
|
return await this.client.list();
|
|
@@ -280,6 +485,88 @@ export class ImapClient {
|
|
|
280
485
|
return info;
|
|
281
486
|
});
|
|
282
487
|
}
|
|
488
|
+
/** Add flags to a message by UID */
|
|
489
|
+
async addFlags(mailbox, uid, flags) {
|
|
490
|
+
try {
|
|
491
|
+
await this.connect();
|
|
492
|
+
await this.client.mailboxOpen(mailbox, { readOnly: false });
|
|
493
|
+
return await this.client.messageFlagsAdd([uid], flags, { uid: true });
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
console.error(`Error adding flags to UID ${uid}: ${error.message}`);
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
await this.client.mailboxClose();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/** Remove flags from a message by UID */
|
|
504
|
+
async removeFlags(mailbox, uid, flags) {
|
|
505
|
+
try {
|
|
506
|
+
await this.connect();
|
|
507
|
+
await this.client.mailboxOpen(mailbox, { readOnly: false });
|
|
508
|
+
return await this.client.messageFlagsRemove([uid], flags, { uid: true });
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.error(`Error removing flags from UID ${uid}: ${error.message}`);
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
await this.client.mailboxClose();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/** Get flags for a message by UID */
|
|
519
|
+
async getFlags(mailbox, uid) {
|
|
520
|
+
try {
|
|
521
|
+
await this.connect();
|
|
522
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
523
|
+
const msg = await this.client.fetchOne(uid, { flags: true }, { uid: true });
|
|
524
|
+
if (!msg)
|
|
525
|
+
return [];
|
|
526
|
+
return [...msg.flags];
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error(`Error getting flags for UID ${uid}: ${error.message}`);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
await this.client.mailboxClose();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** Delete a message by UID from a mailbox */
|
|
537
|
+
async deleteMessageByUid(mailbox, uid) {
|
|
538
|
+
try {
|
|
539
|
+
await this.connect();
|
|
540
|
+
await this.client.mailboxOpen(mailbox, { readOnly: false });
|
|
541
|
+
await this.client.messageDelete([uid], { uid: true });
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
console.error(`Error deleting UID ${uid} from ${mailbox}: ${error.message}`);
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
await this.client.mailboxClose();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** Watch a mailbox for new messages via IMAP IDLE. Calls onNew when messages arrive.
|
|
552
|
+
* Returns a stop function to end the watch. */
|
|
553
|
+
async watchMailbox(mailbox, onNew) {
|
|
554
|
+
await this.connect();
|
|
555
|
+
await this.client.mailboxOpen(mailbox, { readOnly: true });
|
|
556
|
+
const handler = (data) => {
|
|
557
|
+
if (data.count > data.prevCount) {
|
|
558
|
+
onNew(data.count - data.prevCount);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
this.client.on('exists', handler);
|
|
562
|
+
return async () => {
|
|
563
|
+
this.client.off('exists', handler);
|
|
564
|
+
try {
|
|
565
|
+
await this.client.mailboxClose();
|
|
566
|
+
}
|
|
567
|
+
catch { /* ignore */ }
|
|
568
|
+
};
|
|
569
|
+
}
|
|
283
570
|
async logout() {
|
|
284
571
|
this.isDestroying = true;
|
|
285
572
|
this.cleanup();
|
|
@@ -293,6 +580,7 @@ export class ImapClient {
|
|
|
293
580
|
}
|
|
294
581
|
async connect() {
|
|
295
582
|
try {
|
|
583
|
+
await this.initialized; // Ensure async init completed
|
|
296
584
|
if (this.connectionPromise) {
|
|
297
585
|
await this.connectionPromise;
|
|
298
586
|
}
|
|
@@ -301,7 +589,7 @@ export class ImapClient {
|
|
|
301
589
|
}
|
|
302
590
|
}
|
|
303
591
|
catch (err) {
|
|
304
|
-
console.error(
|
|
592
|
+
console.error(`${new Date().toISOString()} Connection failed: ${err.message}`);
|
|
305
593
|
if (!this.isDestroying) {
|
|
306
594
|
await this.handleReconnection();
|
|
307
595
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail-specific IMAP support with OAuth authentication
|
|
3
|
+
*/
|
|
4
|
+
import { ImapClientConfig } from './types.js';
|
|
5
|
+
export interface OAuthImapConfig {
|
|
6
|
+
username: string;
|
|
7
|
+
credentialsPath?: string;
|
|
8
|
+
tokenDirectory?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if email address is Gmail
|
|
12
|
+
*/
|
|
13
|
+
export declare function isGmailUser(username: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Check if a server is Gmail
|
|
16
|
+
*/
|
|
17
|
+
export declare function isGmailServer(server: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Creates an ImapClientConfig with OAuth tokenProvider
|
|
20
|
+
* Auto-detects Gmail from @gmail.com email addresses
|
|
21
|
+
* Tokens stored in <tokenDirectory>/tokens/<sanitized-email>/token.json
|
|
22
|
+
*
|
|
23
|
+
* Credentials and tokens are stored in the iflow package directory by default
|
|
24
|
+
*/
|
|
25
|
+
export declare function createImapConfig(config: OAuthImapConfig): ImapClientConfig;
|
|
26
|
+
/** @deprecated Use createImapConfig instead */
|
|
27
|
+
export declare const createGmailConfig: typeof createImapConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Auto-configure IMAP client - detects Gmail and uses OAuth, otherwise uses password
|
|
30
|
+
* This is the main function to use - it handles all authentication logic internally
|
|
31
|
+
*
|
|
32
|
+
* @param config - Server configuration with username, password (optional for Gmail), server, port
|
|
33
|
+
* @returns ImapClientConfig ready to use with ImapClient
|
|
34
|
+
*/
|
|
35
|
+
export declare function createAutoImapConfig(config: {
|
|
36
|
+
server: string;
|
|
37
|
+
port: number;
|
|
38
|
+
username: string;
|
|
39
|
+
password?: string;
|
|
40
|
+
verbose?: boolean;
|
|
41
|
+
rejectUnauthorized?: boolean;
|
|
42
|
+
}): ImapClientConfig;
|
|
43
|
+
//# sourceMappingURL=gmail.d.ts.map
|