@gera2ld/imap 0.0.1

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/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # IMAP Client
2
+
3
+ A lightweight IMAP client for Node.js that supports real-time email monitoring using IDLE mode.
4
+
5
+ ## Usage
6
+
7
+ ### Basic Example
8
+
9
+ ```typescript
10
+ import { ImapClient } from '@gera2ld/imap';
11
+
12
+ const client = new ImapClient({
13
+ host: 'imap.example.com',
14
+ port: 993,
15
+ secure: true,
16
+ user: 'your-email@example.com',
17
+ password: 'your-password',
18
+ });
19
+
20
+ // Connect and authenticate
21
+ await client.connect();
22
+
23
+ // Select a mailbox
24
+ await client.selectMailbox('INBOX');
25
+
26
+ // Perform operations like fetching messages
27
+ const messages = await client.fetchMessages('1:*', ['UID', 'ENVELOPE']);
28
+ console.log(messages);
29
+
30
+ // Disconnect
31
+ await client.disconnect();
32
+ ```
33
+
34
+ ### Watching for New Mails and Deletions Using IDLE Mode
35
+
36
+ ```typescript
37
+ import { ImapClient } from '@gera2ld/imap';
38
+
39
+ async function watchEmails() {
40
+ const client = new ImapClient({
41
+ host: 'imap.example.com',
42
+ port: 993,
43
+ secure: true,
44
+ user: 'your-email@example.com',
45
+ password: 'your-password',
46
+ });
47
+
48
+ try {
49
+ // Connect and authenticate
50
+ await client.connect();
51
+
52
+ // Select the mailbox to watch (e.g., INBOX)
53
+ await client.selectMailbox('INBOX');
54
+
55
+ // Listen for new emails
56
+ client.on('newmail', (info) => {
57
+ console.log(`New mail received! Total messages in mailbox: ${info.count}`);
58
+ // Handle new email notification
59
+ });
60
+
61
+ // Listen for message deletions
62
+ client.on('expunge', (info) => {
63
+ console.log(`Message deleted! Sequence number: ${info.seq}`);
64
+ // Handle message deletion
65
+ });
66
+
67
+ // Listen for other changes
68
+ client.on('fetch', (message) => {
69
+ console.log(`Message ${message.seq} updated with attributes:`, message.attributes);
70
+ });
71
+
72
+ // Start listening for real-time updates using IDLE mode
73
+ await client.startIdle();
74
+ console.log('Now listening for new emails and deletions...');
75
+
76
+ // Keep the program running to continue listening
77
+ // Set up graceful shutdown
78
+ const handleShutdown = async () => {
79
+ console.log('Shutting down gracefully...');
80
+ try {
81
+ // Stop listening for updates
82
+ await client.stopIdle();
83
+ // Disconnect from the server
84
+ await client.disconnect();
85
+ console.log('Disconnected from IMAP server');
86
+ process.exit(0);
87
+ } catch (err) {
88
+ console.error('Error during shutdown:', err);
89
+ process.exit(1);
90
+ }
91
+ };
92
+
93
+ // Listen for termination signals
94
+ process.on('SIGINT', handleShutdown);
95
+ process.on('SIGTERM', handleShutdown);
96
+
97
+ } catch (error) {
98
+ console.error('Error:', error);
99
+ }
100
+ }
101
+
102
+ // Run the example
103
+ watchEmails();
104
+ ```
105
+
106
+ ## Authentication
107
+
108
+ The client supports multiple authentication methods:
109
+
110
+ ### Password Authentication
111
+ ```typescript
112
+ const client = new ImapClient({
113
+ host: 'imap.example.com',
114
+ port: 993,
115
+ secure: true,
116
+ user: 'your-email@example.com',
117
+ password: 'your-password',
118
+ });
119
+ ```
120
+
121
+ ### XOAUTH2 Authentication
122
+ ```typescript
123
+ const client = new ImapClient({
124
+ host: 'imap.example.com',
125
+ port: 993,
126
+ secure: true,
127
+ user: 'your-email@example.com',
128
+ accessToken: 'your-oauth-token',
129
+ });
130
+
131
+ // Or with a function that returns a token
132
+ const client = new ImapClient({
133
+ host: 'imap.example.com',
134
+ port: 993,
135
+ secure: true,
136
+ user: 'your-email@example.com',
137
+ accessToken: async () => {
138
+ // Return a fresh access token
139
+ return await getFreshAccessToken();
140
+ },
141
+ });
142
+ ```
143
+
144
+ ## Available Events
145
+
146
+ - `newmail`: Emitted when new messages arrive in the mailbox
147
+ - `expunge`: Emitted when messages are deleted from the mailbox
148
+ - `fetch`: Emitted when message attributes are updated
149
+ - `recent`: Emitted when the number of recent messages changes
150
+ - `flags`: Emitted when message flags change
151
+ - `list`: Emitted when mailbox list information is received
152
+ - `response`: Emitted for raw IMAP responses
153
+ - `error`: Emitted when an error occurs
154
+ - `close`: Emitted when the connection is closed
155
+
156
+ ## API
157
+
158
+ ### ImapClient(options)
159
+
160
+ Creates a new IMAP client instance.
161
+
162
+ #### Options
163
+
164
+ - `host` (string): The IMAP server hostname
165
+ - `port` (number): The IMAP server port (typically 143 for non-secure, 993 for secure)
166
+ - `secure` (boolean): Whether to use TLS/SSL connection
167
+ - `user` (string): The username/email address
168
+ - `password` (string, optional): The password for authentication
169
+ - `accessToken` (string | function, optional): Access token for XOAUTH2 authentication
170
+
171
+ ### connect()
172
+
173
+ Connects to the IMAP server and performs authentication.
174
+
175
+ ### disconnect()
176
+
177
+ Disconnects from the IMAP server and cleans up resources.
178
+
179
+ ### selectMailbox(mailbox)
180
+
181
+ Selects a mailbox for subsequent operations.
182
+
183
+ ### listMailboxes()
184
+
185
+ Lists all mailboxes on the server.
186
+
187
+ ### fetchMessages(sequence, [items], [options])
188
+
189
+ Fetches messages from the currently selected mailbox.
190
+ - `items` (optional): The message data items to fetch (e.g., ['UID', 'ENVELOPE', 'BODY.PEEK[]']). Defaults to ['UID', 'ENVELOPE', 'FLAGS']
191
+ - `options` (optional): Additional options for the fetch operation
192
+ - `uid` (boolean, default: true): Whether to use UID-based fetch
193
+
194
+ ### searchMessages(criteria, [options])
195
+
196
+ Searches messages in the currently selected mailbox based on the given criteria.
197
+ - `options` (optional): Additional options for the search operation
198
+ - `uid` (boolean, default: true): Whether to use UID-based search
199
+
200
+ ### setFlags(sequence, operation, flags, [options])
201
+
202
+ Sets flags on messages in the currently selected mailbox.
203
+ - `operation`: The flag operation ('+' to add flags, '-' to remove flags, '=' to replace all flags)
204
+ - `flags`: Array of flags to operate on (e.g., ['\\Seen', '\\Flagged'])
205
+ - `options` (optional): Additional options for the operation
206
+ - `uid` (boolean, default: true): Whether to use UID-based operation
207
+
208
+ ### markAsSeen(sequence, [options])
209
+
210
+ Marks messages as seen in the currently selected mailbox.
211
+ - `options` (optional): Additional options for the operation
212
+ - `uid` (boolean, default: true): Whether to use UID-based operation
213
+
214
+ ### markAsUnseen(sequence, [options])
215
+
216
+ Marks messages as unseen in the currently selected mailbox.
217
+ - `options` (optional): Additional options for the operation
218
+ - `uid` (boolean, default: true): Whether to use UID-based operation
219
+
220
+ ### deleteMessages(sequence, [options])
221
+
222
+ Deletes messages in the currently selected mailbox.
223
+ - `options` (optional): Additional options for the operation
224
+ - `uid` (boolean, default: true): Whether to use UID-based operation
225
+
226
+ ### moveMessages(sequence, destinationMailbox, [options])
227
+
228
+ Moves messages to a different mailbox.
229
+ - `options` (optional): Additional options for the operation
230
+ - `uid` (boolean, default: true): Whether to use UID-based operation
231
+
232
+ ### startIdle()
233
+
234
+ Manually starts IDLE mode for real-time updates.
235
+
236
+ ### stopIdle()
237
+
238
+ Manually stops IDLE mode.
239
+
240
+ ## Utilities
241
+
242
+ The package also exports utility functions for working with IMAP:
243
+
244
+ ### formatDateForImap(date)
245
+
246
+ Formats a Date object to the IMAP date format used in SEARCH commands (e.g., SINCE, ON).
247
+ The format is "DD-MMM-YYYY" (e.g., "01-Jan-2023").
248
+
249
+ ```typescript
250
+ import { formatDateForImap } from '@gera2ld/imap';
251
+
252
+ const date = new Date('2023-01-15');
253
+ const imapDate = formatDateForImap(date); // "15-Jan-2023"
254
+
255
+ // Use in search commands
256
+ await client.searchMessages(`SINCE ${imapDate}`);
257
+ ```
258
+
259
+ ### formatDateTimeForImap(date)
260
+
261
+ Formats a Date object to the IMAP datetime format used in SEARCH commands.
262
+ The format is "DD-MMM-YYYY HH:MM:SS +ZZZZ" (e.g., "01-Jan-2023 12:00:00 +0000").
263
+
264
+ ```typescript
265
+ import { formatDateTimeForImap } from '@gera2ld/imap';
266
+
267
+ const date = new Date('2023-01-15T14:30:00Z');
268
+ const imapDateTime = formatDateTimeForImap(date); // "15-Jan-2023 14:30:00 +0000"
269
+ ```
270
+
271
+ ## License
272
+
273
+ MIT
@@ -0,0 +1,129 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { ParsedResponse } from './parser';
3
+ import type { FetchMessage, ImapClientOptions, MailboxInfo } from './types';
4
+ type ImapClientEvents = {
5
+ newmail: [{
6
+ count: number;
7
+ }];
8
+ expunge: [{
9
+ seq: number;
10
+ }];
11
+ recent: [{
12
+ count: number;
13
+ }];
14
+ flags: [{
15
+ flags: any[];
16
+ }];
17
+ response: [ParsedResponse];
18
+ send: [string];
19
+ error: [Error];
20
+ close: [];
21
+ };
22
+ export declare class ImapClient extends EventEmitter<ImapClientEvents> {
23
+ private options;
24
+ private connector;
25
+ private parser;
26
+ private tagCounter;
27
+ private tagPrefix;
28
+ private pendingCommand;
29
+ constructor(options: ImapClientOptions);
30
+ private nextTag;
31
+ /**
32
+ * Register a pending command and return a promise that resolves when the response arrives.
33
+ * Throws if another command is already pending.
34
+ */
35
+ private registerPendingCommand;
36
+ /**
37
+ * Send a raw command to the server.
38
+ */
39
+ private sendCommand;
40
+ /**
41
+ * Execute a command and wait for its response.
42
+ * Processes matching untagged responses as they arrive via onResponse, filters out nulls.
43
+ * Non-matching responses are emitted as events.
44
+ *
45
+ * @param command - The IMAP command to send (e.g., 'LIST "" "*"', 'UID SEARCH ALL')
46
+ * @param matchCommand - Optional command name to match in responses (e.g., 'LIST', 'SEARCH', 'FETCH'). Required when onResponse is provided.
47
+ * @param onResponse - Optional function to transform each matching untagged response as it arrives. Return null to skip.
48
+ * @returns Promise with tagged response and array of transformed items
49
+ */
50
+ private executeCommand;
51
+ /**
52
+ * Handle incoming data lines from the connector.
53
+ */
54
+ private handleResponse;
55
+ private handleAsyncNotification;
56
+ private parseFetchAttributes;
57
+ /**
58
+ * Connect to the IMAP server and authenticate.
59
+ */
60
+ connect(): Promise<void>;
61
+ private processResponses;
62
+ /**
63
+ * Authenticate with the server.
64
+ */
65
+ login(): Promise<void>;
66
+ /**
67
+ * List all mailboxes.
68
+ */
69
+ listMailboxes(): Promise<MailboxInfo[]>;
70
+ /**
71
+ * Select a mailbox.
72
+ */
73
+ selectMailbox(mailbox: string): Promise<void>;
74
+ /**
75
+ * Search messages.
76
+ */
77
+ searchMessages(criteria: string, options?: Partial<{
78
+ uid: boolean;
79
+ }>): Promise<number[]>;
80
+ /**
81
+ * Fetch messages.
82
+ */
83
+ fetchMessages(sequence: string, items?: string[], options?: Partial<{
84
+ uid: boolean;
85
+ }>): Promise<FetchMessage[]>;
86
+ /**
87
+ * Set flags on messages.
88
+ */
89
+ setFlags(sequence: string, operation: '+' | '-' | '=', flags: string[], options?: Partial<{
90
+ uid: boolean;
91
+ }>): Promise<void>;
92
+ /**
93
+ * Mark messages as seen.
94
+ */
95
+ markAsSeen(sequence: string, options?: Partial<{
96
+ uid: boolean;
97
+ }>): Promise<void>;
98
+ /**
99
+ * Mark messages as unseen.
100
+ */
101
+ markAsUnseen(sequence: string, options?: Partial<{
102
+ uid: boolean;
103
+ }>): Promise<void>;
104
+ /**
105
+ * Delete messages.
106
+ */
107
+ deleteMessages(sequence: string, options?: Partial<{
108
+ uid: boolean;
109
+ }>): Promise<void>;
110
+ /**
111
+ * Move messages to another mailbox.
112
+ */
113
+ moveMessages(sequence: string, destinationMailbox: string, options?: Partial<{
114
+ uid: boolean;
115
+ }>): Promise<void>;
116
+ /**
117
+ * Start IDLE mode.
118
+ */
119
+ startIdle(): Promise<void>;
120
+ /**
121
+ * Stop IDLE mode.
122
+ */
123
+ stopIdle(): Promise<void>;
124
+ /**
125
+ * Disconnect from the server.
126
+ */
127
+ disconnect(): Promise<void>;
128
+ }
129
+ export {};
@@ -0,0 +1,20 @@
1
+ import { BufferReader } from '@gera2ld/common';
2
+ export interface ConnectorOptions {
3
+ host: string;
4
+ port: number;
5
+ secure: boolean;
6
+ }
7
+ export interface ConnectorCallbacks {
8
+ onError?: (err: Error) => void;
9
+ onClose?: () => void;
10
+ }
11
+ export declare class Connector {
12
+ private options;
13
+ private _socket;
14
+ reader: BufferReader;
15
+ isConnected: boolean;
16
+ constructor(options: ConnectorOptions);
17
+ connect(callbacks?: ConnectorCallbacks): Promise<void>;
18
+ write(data: string): void;
19
+ disconnect(): void;
20
+ }
@@ -0,0 +1,3 @@
1
+ export * from './client';
2
+ export * from './parser';
3
+ export * from './util';
package/dist/index.js ADDED
@@ -0,0 +1,642 @@
1
+ import { EventEmitter as S } from "node:events";
2
+ import * as T from "node:net";
3
+ import * as $ from "node:tls";
4
+ const b = "=", w = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5
+ let g;
6
+ function I() {
7
+ return g ||= O(w), g;
8
+ }
9
+ function O(r) {
10
+ const t = new Array(255);
11
+ for (let e = 0; e < r.length; e += 1)
12
+ t[r.charCodeAt(e)] = e;
13
+ return t;
14
+ }
15
+ function M(r, t) {
16
+ const e = t[r];
17
+ if (e == null) throw new Error("Unable to parse base64 string.");
18
+ return e;
19
+ }
20
+ function D(r, t, e) {
21
+ let s = "", n = 0;
22
+ for (; n < r.length; ) {
23
+ const o = r[n++], i = r[n++], a = r[n++];
24
+ if (s += t[o >> 2], s += t[(o & 3) << 4 | (i || 0) >> 4], i == null ? s += b : s += t[(i & 15) << 2 | (a || 0) >> 6], a == null) {
25
+ s += b;
26
+ break;
27
+ }
28
+ s += t[a & 63];
29
+ }
30
+ return s;
31
+ }
32
+ function v(r, t) {
33
+ let e = r.length;
34
+ for (; e > 0 && r[e - 1] === "="; ) e -= 1;
35
+ const s = Math.ceil(e / 4), n = new Uint8Array(3 * s);
36
+ let o = 0, i = 0, a = 0;
37
+ for (i = 0; i < s; i += 1) {
38
+ let l = 0;
39
+ for (a = 0; a < 4; a += 1) {
40
+ o = i * 4 + a;
41
+ const f = r[o];
42
+ if (!f || f === b) break;
43
+ l |= M(r.charCodeAt(o), t) << (3 - a) * 6;
44
+ }
45
+ n[i * 3] = l >> 16, n[i * 3 + 1] = l >> 8 & 255, n[i * 3 + 2] = l & 255;
46
+ }
47
+ if (o < e - 1 || a === 1) throw new Error("Invalid base64 string");
48
+ let c = 3 * s;
49
+ for (; a < 4; a += 1) c -= 1;
50
+ return n.subarray(0, c);
51
+ }
52
+ function U(r) {
53
+ return D(r, w);
54
+ }
55
+ function y(r) {
56
+ return v(r, I());
57
+ }
58
+ function N(r) {
59
+ return new TextEncoder().encode(r);
60
+ }
61
+ function E(r) {
62
+ return new TextDecoder().decode(r);
63
+ }
64
+ function m() {
65
+ const r = {
66
+ status: "pending"
67
+ };
68
+ return r.promise = new Promise((t, e) => {
69
+ r.resolve = t, r.reject = e;
70
+ }), r.promise.then(
71
+ () => {
72
+ r.status = "resolved";
73
+ },
74
+ () => {
75
+ r.status = "rejected";
76
+ }
77
+ ), r;
78
+ }
79
+ const F = new Uint8Array([13, 10]);
80
+ class L {
81
+ buffer = new Uint8Array(0);
82
+ requests = [];
83
+ isClosed = !1;
84
+ read(t = 8192) {
85
+ const e = m();
86
+ return this.requests.push({
87
+ deferred: e,
88
+ handle: () => {
89
+ const s = Math.min(t, this.buffer.length), n = this.buffer.subarray(0, s);
90
+ return this.buffer = this.buffer.subarray(s), e.resolve(n), !0;
91
+ }
92
+ }), this.processBuffer(), e.promise;
93
+ }
94
+ readExact(t) {
95
+ const e = m();
96
+ return this.requests.push({
97
+ deferred: e,
98
+ handle: () => {
99
+ if (t > this.buffer.length) return !1;
100
+ const s = this.buffer.subarray(0, t);
101
+ return this.buffer = this.buffer.subarray(t), e.resolve(s), !0;
102
+ }
103
+ }), this.processBuffer(), e.promise;
104
+ }
105
+ readUntil(t = F) {
106
+ typeof t == "string" && (t = N(t));
107
+ const e = m();
108
+ return this.requests.push({
109
+ deferred: e,
110
+ handle: () => {
111
+ const s = P(this.buffer, t);
112
+ if (s < 0) return !1;
113
+ const n = s + t.length, o = this.buffer.subarray(0, n);
114
+ return this.buffer = this.buffer.subarray(n), e.resolve(o), !0;
115
+ }
116
+ }), this.processBuffer(), e.promise;
117
+ }
118
+ feed(t) {
119
+ if (this.isClosed) throw new Error("Buffer is closed");
120
+ this.buffer = B(this.buffer, t), this.processBuffer();
121
+ }
122
+ close() {
123
+ this.isClosed = !0, this.processBuffer();
124
+ }
125
+ processBuffer() {
126
+ for (; this.requests.length; ) {
127
+ const t = this.requests[0];
128
+ if (!t.handle())
129
+ if (this.isClosed)
130
+ t.deferred.reject(new Error("Buffer is closed"));
131
+ else break;
132
+ this.requests.shift();
133
+ }
134
+ }
135
+ }
136
+ function B(r, t) {
137
+ const e = new Uint8Array(r.length + t.length);
138
+ return e.set(r, 0), e.set(t, r.length), e;
139
+ }
140
+ function P(r, t) {
141
+ if (!t.length) return 0;
142
+ for (let e = 0; e <= r.length - t.length; e += 1) {
143
+ let s;
144
+ for (s = 0; s < t.length && r[e + s] === t[s]; s += 1)
145
+ ;
146
+ if (s === t.length) return e;
147
+ }
148
+ return -1;
149
+ }
150
+ class R {
151
+ options;
152
+ _socket = null;
153
+ reader = new L();
154
+ isConnected = !1;
155
+ constructor(t) {
156
+ this.options = t;
157
+ }
158
+ async connect(t) {
159
+ return new Promise((e, s) => {
160
+ const n = this.options.secure ? $.connect({
161
+ host: this.options.host,
162
+ port: this.options.port,
163
+ rejectUnauthorized: !1
164
+ }) : T.connect({
165
+ host: this.options.host,
166
+ port: this.options.port
167
+ });
168
+ this._socket = n, n.on("data", (o) => {
169
+ this.reader.feed(o);
170
+ }), n.on("connect", () => {
171
+ this.isConnected = !0, e();
172
+ }), n.on("error", (o) => {
173
+ t?.onError?.(o), s(o);
174
+ }), n.on("close", () => {
175
+ this.isConnected = !1, t?.onClose?.();
176
+ });
177
+ });
178
+ }
179
+ write(t) {
180
+ if (!this._socket)
181
+ throw new Error("Not connected");
182
+ this._socket.write(t);
183
+ }
184
+ disconnect() {
185
+ this._socket && (this._socket.end(), this._socket.destroy(), this._socket = null);
186
+ }
187
+ }
188
+ class j {
189
+ decode(t) {
190
+ return new TextDecoder().decode(t);
191
+ }
192
+ async *parse(t) {
193
+ for (; ; ) {
194
+ const e = await t.readUntil();
195
+ if (e.length === 0) break;
196
+ const s = this.decode(e);
197
+ if (s.startsWith("+ "))
198
+ yield {
199
+ symbol: "+",
200
+ text: s.slice(2)
201
+ };
202
+ else {
203
+ const n = s.split(" ");
204
+ let o = "", i, a;
205
+ s.startsWith("* ") ? (o = "*", n.shift(), isNaN(+n[0]) ? a = n.shift() : a = n.splice(1, 1)[0]) : (i = n.shift(), a = n.shift());
206
+ let c = n.join(" ");
207
+ const l = {
208
+ command: a,
209
+ stack: [[]],
210
+ brackets: []
211
+ };
212
+ for (; c; ) {
213
+ this.parseAttributes(l, c);
214
+ const f = l.stack.at(-1), h = f.at(-1), p = typeof h == "string" && h.match(/^\{(\d+)\}\r\n$/);
215
+ if (p) {
216
+ const x = +p[1], A = this.decode(await t.readExact(x));
217
+ f.pop(), f.push(A);
218
+ }
219
+ if (!l.brackets.length) break;
220
+ c = this.decode(await t.readUntil());
221
+ }
222
+ yield o ? {
223
+ symbol: o,
224
+ command: a,
225
+ attributes: l.stack[0]
226
+ } : {
227
+ symbol: "",
228
+ tag: i,
229
+ command: a,
230
+ attributes: l.stack[0]
231
+ };
232
+ }
233
+ }
234
+ }
235
+ parseAttributes(t, e) {
236
+ let s = !1, n = !0, o = !1, i = "";
237
+ const a = () => {
238
+ if (!i) return;
239
+ let c;
240
+ isNaN(+i) ? i.toLowerCase() === "nil" ? c = null : c = i : c = +i, t.stack.at(-1).push(c), i = "";
241
+ };
242
+ for (let c = 0; c < e.length; c += 1) {
243
+ const l = e[c];
244
+ if (s)
245
+ l === '"' ? (s = !1, a()) : (l === "\\" && (c += 1), i += e[c]);
246
+ else if (l === '"')
247
+ s = !0;
248
+ else if (l === " ")
249
+ a();
250
+ else if ((n ? "([" : "(").includes(l)) {
251
+ t.brackets.push(l);
252
+ const f = [];
253
+ t.stack.at(-1).push(f), t.stack.push(f), l === "[" && (o = !0);
254
+ } else if ((o ? "])" : ")").includes(l)) {
255
+ const f = t.brackets.pop();
256
+ if (!["()", "[]"].includes(f + l))
257
+ throw new Error("Invalid bracket");
258
+ a(), t.stack.pop(), l === "]" && (o = !1);
259
+ } else
260
+ i += e[c];
261
+ n = !1;
262
+ }
263
+ a();
264
+ }
265
+ }
266
+ const k = [
267
+ "Jan",
268
+ "Feb",
269
+ "Mar",
270
+ "Apr",
271
+ "May",
272
+ "Jun",
273
+ "Jul",
274
+ "Aug",
275
+ "Sep",
276
+ "Oct",
277
+ "Nov",
278
+ "Dec"
279
+ ];
280
+ function H(r) {
281
+ const t = String(r.getDate()).padStart(2, "0"), e = k[r.getMonth()], s = r.getFullYear();
282
+ return `${t}-${e}-${s}`;
283
+ }
284
+ function z(r) {
285
+ const t = String(r.getDate()).padStart(2, "0"), e = k[r.getMonth()], s = r.getFullYear(), n = String(r.getHours()).padStart(2, "0"), o = String(r.getMinutes()).padStart(2, "0"), i = String(r.getSeconds()).padStart(2, "0"), a = -r.getTimezoneOffset(), c = a >= 0 ? "+" : "-", l = Math.abs(a), f = String(Math.floor(l / 60)).padStart(
286
+ 2,
287
+ "0"
288
+ ), h = String(l % 60).padStart(2, "0"), p = `${c}${f}${h}`;
289
+ return `${t}-${e}-${s} ${n}:${o}:${i} ${p}`;
290
+ }
291
+ function d(r) {
292
+ return !Array.isArray(r) || r.length < 4 ? {} : {
293
+ name: C(r[0] ?? "") || void 0,
294
+ adl: r[1] ?? void 0,
295
+ mailbox: r[2] ?? void 0,
296
+ host: r[3] ?? void 0
297
+ };
298
+ }
299
+ function C(r) {
300
+ return r.split(/\s+/).map((t) => {
301
+ if (t.startsWith("=?") && t.endsWith("?=")) {
302
+ const [e, s, n] = t.slice(2, -2).split("?");
303
+ if (e.toLowerCase() !== "utf-8")
304
+ throw new Error(`Charset is not supported: ${e}`);
305
+ let o;
306
+ if (s === "B")
307
+ o = y(n);
308
+ else {
309
+ const i = [];
310
+ for (let a = 0; a < n.length; a += 1)
311
+ if (n[a] === "=") {
312
+ const c = n.slice(a + 1, a + 3);
313
+ i.push(parseInt(c, 16)), a += 2;
314
+ } else
315
+ i.push(n.charCodeAt(a));
316
+ o = new Uint8Array(i);
317
+ }
318
+ return E(o);
319
+ }
320
+ return t;
321
+ }).join("");
322
+ }
323
+ const u = { uid: !0 };
324
+ function _(r, t) {
325
+ const e = `user=${r}auth=Bearer ${t}`;
326
+ return U(new TextEncoder().encode(e));
327
+ }
328
+ class G extends S {
329
+ options;
330
+ connector;
331
+ parser;
332
+ tagCounter = 0;
333
+ tagPrefix = "A";
334
+ pendingCommand = null;
335
+ constructor(t) {
336
+ super(), this.options = t, this.parser = new j(), this.connector = new R({
337
+ host: t.host,
338
+ port: t.port,
339
+ secure: t.secure
340
+ });
341
+ }
342
+ nextTag() {
343
+ return this.tagPrefix + this.tagCounter++;
344
+ }
345
+ /**
346
+ * Register a pending command and return a promise that resolves when the response arrives.
347
+ * Throws if another command is already pending.
348
+ */
349
+ registerPendingCommand(t, e, s) {
350
+ if (this.pendingCommand)
351
+ throw new Error(
352
+ `Cannot execute ${e} command while another operation ("${this.pendingCommand.command}") is in progress.`
353
+ );
354
+ const n = m();
355
+ return this.pendingCommand = {
356
+ tag: t,
357
+ command: e,
358
+ callback: s,
359
+ deferred: n
360
+ }, n.promise.finally(() => {
361
+ this.pendingCommand = null;
362
+ }), n.promise;
363
+ }
364
+ /**
365
+ * Send a raw command to the server.
366
+ */
367
+ sendCommand(t) {
368
+ this.emit("send", t), this.connector.write(`${t}\r
369
+ `);
370
+ }
371
+ /**
372
+ * Execute a command and wait for its response.
373
+ * Processes matching untagged responses as they arrive via onResponse, filters out nulls.
374
+ * Non-matching responses are emitted as events.
375
+ *
376
+ * @param command - The IMAP command to send (e.g., 'LIST "" "*"', 'UID SEARCH ALL')
377
+ * @param matchCommand - Optional command name to match in responses (e.g., 'LIST', 'SEARCH', 'FETCH'). Required when onResponse is provided.
378
+ * @param onResponse - Optional function to transform each matching untagged response as it arrives. Return null to skip.
379
+ * @returns Promise with tagged response and array of transformed items
380
+ */
381
+ async executeCommand(t, e) {
382
+ const s = this.nextTag(), n = t.split(" ");
383
+ let o = n.shift();
384
+ if (o === "UID" && (o = n.shift()), !o) throw new Error("Invalid command");
385
+ const i = this.registerPendingCommand(s, o, e);
386
+ this.sendCommand(`${s} ${t}`), await i;
387
+ }
388
+ /**
389
+ * Handle incoming data lines from the connector.
390
+ */
391
+ handleResponse(t) {
392
+ this.emit("response", t);
393
+ const e = this.pendingCommand, s = () => {
394
+ t.symbol === "*" && this.handleAsyncNotification(t);
395
+ };
396
+ if (e) {
397
+ const n = () => {
398
+ t.symbol === "" && t.tag === e.tag ? t.command === "OK" ? e.deferred.resolve() : e.deferred.reject(new Error(t.command)) : s();
399
+ };
400
+ e.callback ? e.callback({
401
+ response: t,
402
+ pending: e,
403
+ next: s,
404
+ fallback: n
405
+ }) : n();
406
+ } else
407
+ s();
408
+ }
409
+ handleAsyncNotification(t) {
410
+ switch (t.command) {
411
+ case "EXISTS":
412
+ this.emit("newmail", { count: t.attributes[0] });
413
+ break;
414
+ case "EXPUNGE":
415
+ this.emit("expunge", { seq: t.attributes[0] });
416
+ break;
417
+ case "RECENT":
418
+ this.emit("recent", { count: t.attributes[0] });
419
+ break;
420
+ case "FLAGS":
421
+ this.emit("flags", { flags: t.attributes[0] });
422
+ break;
423
+ }
424
+ }
425
+ parseFetchAttributes(t, e) {
426
+ const s = { seq: t };
427
+ if (Array.isArray(e))
428
+ for (let n = 0; n < e.length; n += 2) {
429
+ const o = e[n]?.toUpperCase(), i = e[n + 1];
430
+ switch (o) {
431
+ case "UID":
432
+ s.uid = i;
433
+ break;
434
+ case "FLAGS":
435
+ s.flags = Array.isArray(i) ? i : [i];
436
+ break;
437
+ case "ENVELOPE":
438
+ s.envelope = {
439
+ date: i?.[0],
440
+ subject: C(i?.[1]),
441
+ from: i?.[2]?.map(d) ?? [],
442
+ sender: i?.[3]?.map(d) ?? [],
443
+ replyTo: i?.[4]?.map(d) ?? [],
444
+ to: i?.[5]?.map(d) ?? [],
445
+ cc: i?.[6]?.map(d) ?? [],
446
+ bcc: i?.[7]?.map(d) ?? [],
447
+ inReplyTo: i?.[8],
448
+ messageId: i?.[9]
449
+ };
450
+ break;
451
+ case "INTERNALDATE":
452
+ s.internalDate = i;
453
+ break;
454
+ case "RFC822.SIZE":
455
+ s.size = i;
456
+ break;
457
+ case "BODY[]":
458
+ s.body = i;
459
+ break;
460
+ case "RFC822.TEXT":
461
+ case "BODY[TEXT]":
462
+ s.text = i;
463
+ break;
464
+ case "BODY[HTML]":
465
+ s.html = i;
466
+ break;
467
+ default:
468
+ s.attributes ||= {}, s.attributes[o] = i;
469
+ break;
470
+ }
471
+ }
472
+ return s;
473
+ }
474
+ /**
475
+ * Connect to the IMAP server and authenticate.
476
+ */
477
+ async connect() {
478
+ await this.connector.connect({
479
+ onError: (t) => this.emit("error", t),
480
+ onClose: () => this.emit("close")
481
+ }), this.processResponses(), await this.registerPendingCommand("", "", ({ response: t, pending: e, next: s }) => {
482
+ t.symbol === "*" && t.command === "OK" ? e.deferred.resolve() : s();
483
+ }), await this.login();
484
+ }
485
+ async processResponses() {
486
+ for await (const t of this.parser.parse(this.connector.reader))
487
+ this.handleResponse(t);
488
+ }
489
+ /**
490
+ * Authenticate with the server.
491
+ */
492
+ async login() {
493
+ if (this.options.accessToken) {
494
+ const t = typeof this.options.accessToken == "function" ? await this.options.accessToken() : this.options.accessToken, e = _(this.options.user, t);
495
+ await this.executeCommand(
496
+ `AUTHENTICATE XOAUTH2 ${e}`,
497
+ ({ response: s, pending: n, fallback: o }) => {
498
+ s.symbol === "+" ? n.deferred.reject(
499
+ new Error(E(y(s.text)))
500
+ ) : o();
501
+ }
502
+ );
503
+ } else if (this.options.password)
504
+ await this.executeCommand(
505
+ `LOGIN "${this.options.user}" "${this.options.password}"`
506
+ );
507
+ else
508
+ throw new Error("Either password or accessToken must be provided");
509
+ }
510
+ /**
511
+ * List all mailboxes.
512
+ */
513
+ async listMailboxes() {
514
+ const t = [];
515
+ return await this.executeCommand('LIST "" "*"', ({ response: e, fallback: s }) => {
516
+ if (e.symbol === "*" && e.command === "LIST") {
517
+ const n = {
518
+ attributes: e.attributes[0],
519
+ delimiter: e.attributes[1],
520
+ name: e.attributes[2]
521
+ };
522
+ t.push(n);
523
+ } else
524
+ s();
525
+ }), t;
526
+ }
527
+ /**
528
+ * Select a mailbox.
529
+ */
530
+ async selectMailbox(t) {
531
+ await this.executeCommand(`SELECT "${t}"`);
532
+ }
533
+ /**
534
+ * Search messages.
535
+ */
536
+ async searchMessages(t, e) {
537
+ const n = { ...u, ...e }.uid ? "UID " : "";
538
+ let o = [];
539
+ return await this.executeCommand(
540
+ `${n}SEARCH ${t}`,
541
+ ({ response: i, fallback: a }) => {
542
+ i.symbol === "*" && i.command === "SEARCH" ? o = [...o, ...i.attributes] : a();
543
+ }
544
+ ), o;
545
+ }
546
+ /**
547
+ * Fetch messages.
548
+ */
549
+ async fetchMessages(t, e = ["UID", "ENVELOPE", "FLAGS"], s) {
550
+ const i = `${{ ...u, ...s }.uid ? "UID " : ""}FETCH ${t} (${e.join(" ")})`, a = [];
551
+ return await this.executeCommand(i, ({ response: c, fallback: l }) => {
552
+ if (c.symbol === "*" && c.command === "FETCH") {
553
+ const f = c.attributes[0], h = c.attributes[1];
554
+ a.push(this.parseFetchAttributes(f, h));
555
+ } else
556
+ l();
557
+ }), a;
558
+ }
559
+ /**
560
+ * Set flags on messages.
561
+ */
562
+ async setFlags(t, e, s, n) {
563
+ const i = { ...u, ...n }.uid ? "UID " : "", a = s.join(" ");
564
+ await this.executeCommand(
565
+ `${i}STORE ${t} ${e}FLAGS (${a})`
566
+ );
567
+ }
568
+ /**
569
+ * Mark messages as seen.
570
+ */
571
+ async markAsSeen(t, e) {
572
+ const s = { ...u, ...e };
573
+ await this.setFlags(t, "+", ["\\Seen"], s);
574
+ }
575
+ /**
576
+ * Mark messages as unseen.
577
+ */
578
+ async markAsUnseen(t, e) {
579
+ const s = { ...u, ...e };
580
+ await this.setFlags(t, "-", ["\\Seen"], s);
581
+ }
582
+ /**
583
+ * Delete messages.
584
+ */
585
+ async deleteMessages(t, e) {
586
+ const s = { ...u, ...e };
587
+ await this.setFlags(t, "+", ["\\Deleted"], s), await this.executeCommand("EXPUNGE");
588
+ }
589
+ /**
590
+ * Move messages to another mailbox.
591
+ */
592
+ async moveMessages(t, e, s) {
593
+ const o = { ...u, ...s }.uid ? "UID " : "";
594
+ await this.executeCommand(
595
+ `${o}MOVE ${t} "${e}"`
596
+ );
597
+ }
598
+ /**
599
+ * Start IDLE mode.
600
+ */
601
+ async startIdle() {
602
+ const t = m();
603
+ let e = !1;
604
+ return this.executeCommand("IDLE", ({ response: s, pending: n, fallback: o }) => {
605
+ if (e)
606
+ o();
607
+ else if (s.symbol === "+")
608
+ e = !0, t.resolve();
609
+ else {
610
+ const i = new Error(s.command);
611
+ t.reject(i), n.deferred.resolve(t.promise);
612
+ }
613
+ }), t.promise;
614
+ }
615
+ /**
616
+ * Stop IDLE mode.
617
+ */
618
+ async stopIdle() {
619
+ if (this.pendingCommand?.command !== "IDLE")
620
+ throw new Error("IDLE mode is not active");
621
+ return this.sendCommand("DONE"), this.pendingCommand.deferred.promise;
622
+ }
623
+ /**
624
+ * Disconnect from the server.
625
+ */
626
+ async disconnect() {
627
+ if (this.pendingCommand?.command === "IDLE")
628
+ try {
629
+ await this.stopIdle();
630
+ } catch {
631
+ }
632
+ this.connector.disconnect();
633
+ }
634
+ }
635
+ export {
636
+ G as ImapClient,
637
+ j as ImapParser,
638
+ C as decodeRfc2047,
639
+ H as formatDateForImap,
640
+ z as formatDateTimeForImap,
641
+ d as parseAddress
642
+ };
@@ -0,0 +1,22 @@
1
+ import type { BufferReader } from '@gera2ld/common';
2
+ export interface TaggedResponse {
3
+ symbol: '';
4
+ tag: string;
5
+ command: string;
6
+ attributes: any[];
7
+ }
8
+ export interface UntaggedResponse {
9
+ symbol: '*';
10
+ command: string;
11
+ attributes: any[];
12
+ }
13
+ export interface ContinuationResponse {
14
+ symbol: '+';
15
+ text: string;
16
+ }
17
+ export type ParsedResponse = TaggedResponse | UntaggedResponse | ContinuationResponse;
18
+ export declare class ImapParser {
19
+ private decode;
20
+ parse(reader: BufferReader): AsyncGenerator<ParsedResponse, void, unknown>;
21
+ private parseAttributes;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ export interface ImapClientOptions {
2
+ host: string;
3
+ port: number;
4
+ secure: boolean;
5
+ user: string;
6
+ password?: string;
7
+ accessToken?: string | (() => Promise<string>);
8
+ debug?: boolean;
9
+ }
10
+ export interface Address {
11
+ name?: string;
12
+ adl?: string;
13
+ mailbox?: string;
14
+ host?: string;
15
+ }
16
+ export interface MessageEnvelope {
17
+ date?: string;
18
+ subject?: string;
19
+ from?: Address[];
20
+ sender?: Address[];
21
+ replyTo?: Address[];
22
+ to?: Address[];
23
+ cc?: Address[];
24
+ bcc?: Address[];
25
+ inReplyTo?: string;
26
+ messageId?: string;
27
+ }
28
+ export interface FetchMessage {
29
+ seq: number;
30
+ attributes?: Record<string, any>;
31
+ uid?: number;
32
+ flags?: string[];
33
+ envelope?: MessageEnvelope;
34
+ body?: any;
35
+ text?: string;
36
+ html?: string;
37
+ internalDate?: string;
38
+ size?: number;
39
+ }
40
+ export interface MailboxInfo {
41
+ attributes: string[];
42
+ delimiter: string;
43
+ name: string;
44
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Address } from './types';
2
+ /**
3
+ * Formats a Date object to the IMAP date format used in SEARCH commands (e.g., SINCE, ON).
4
+ * The format is "DD-MMM-YYYY" (e.g., "01-Jan-2023").
5
+ *
6
+ * @param date The date to format
7
+ * @returns The formatted date string in IMAP format
8
+ */
9
+ export declare function formatDateForImap(date: Date): string;
10
+ /**
11
+ * Formats a Date object to the IMAP datetime format used in SEARCH commands.
12
+ * The format is "DD-MMM-YYYY HH:MM:SS +ZZZZ" (e.g., "01-Jan-2023 12:00:00 +0000").
13
+ *
14
+ * @param date The date to format
15
+ * @returns The formatted datetime string in IMAP format
16
+ */
17
+ export declare function formatDateTimeForImap(date: Date): string;
18
+ export declare function parseAddress(addressComponent: (string | null)[]): Address;
19
+ export declare function decodeRfc2047(str: string): string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@gera2ld/imap",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/index.js",
8
+ "types": "./dist/index.d.ts"
9
+ },
10
+ "./utils": {
11
+ "import": "./dist/utils.js",
12
+ "types": "./dist/utils.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "devDependencies": {
23
+ "@gera2ld/common": "^0.0.1"
24
+ },
25
+ "scripts": {
26
+ "clean": "del-cli dist tsconfig.tsbuildinfo",
27
+ "build:types": "tsc",
28
+ "build:js": "vite build",
29
+ "build": "pnpm clean && pnpm /^build:/"
30
+ }
31
+ }