@bobfrankston/iflow 1.0.33 → 1.0.37
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/gmail.js +6 -1
- 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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP protocol parser and command builder.
|
|
3
|
+
* Pure string logic — no I/O, no Node.js dependencies.
|
|
4
|
+
* Works in browser, Node.js, or worker thread.
|
|
5
|
+
*/
|
|
6
|
+
/** Parsed IMAP response line */
|
|
7
|
+
export interface ImapResponse {
|
|
8
|
+
/** Tag ("*" for untagged, "+" for continuation, or the command tag) */
|
|
9
|
+
tag: string;
|
|
10
|
+
/** Status (OK, NO, BAD, BYE, PREAUTH) or response type (EXISTS, RECENT, FETCH, etc.) */
|
|
11
|
+
type: string;
|
|
12
|
+
/** The full text after the tag and type */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Raw line */
|
|
15
|
+
raw: string;
|
|
16
|
+
}
|
|
17
|
+
/** Parsed FETCH response data */
|
|
18
|
+
export interface FetchData {
|
|
19
|
+
seq: number;
|
|
20
|
+
uid?: number;
|
|
21
|
+
flags?: Set<string>;
|
|
22
|
+
internalDate?: Date;
|
|
23
|
+
size?: number;
|
|
24
|
+
envelope?: EnvelopeData;
|
|
25
|
+
bodyStructure?: any;
|
|
26
|
+
headers?: string;
|
|
27
|
+
source?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Parsed ENVELOPE data */
|
|
30
|
+
export interface EnvelopeData {
|
|
31
|
+
date: Date | null;
|
|
32
|
+
subject: string;
|
|
33
|
+
from: AddressData[];
|
|
34
|
+
sender: AddressData[];
|
|
35
|
+
replyTo: AddressData[];
|
|
36
|
+
to: AddressData[];
|
|
37
|
+
cc: AddressData[];
|
|
38
|
+
bcc: AddressData[];
|
|
39
|
+
inReplyTo: string;
|
|
40
|
+
messageId: string;
|
|
41
|
+
}
|
|
42
|
+
export interface AddressData {
|
|
43
|
+
name: string;
|
|
44
|
+
address: string;
|
|
45
|
+
}
|
|
46
|
+
/** Parsed folder LIST data */
|
|
47
|
+
export interface ListData {
|
|
48
|
+
flags: string[];
|
|
49
|
+
delimiter: string;
|
|
50
|
+
path: string;
|
|
51
|
+
}
|
|
52
|
+
/** Parsed STATUS data */
|
|
53
|
+
export interface StatusData {
|
|
54
|
+
messages?: number;
|
|
55
|
+
recent?: number;
|
|
56
|
+
uidNext?: number;
|
|
57
|
+
uidValidity?: number;
|
|
58
|
+
unseen?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Generate a unique command tag */
|
|
61
|
+
export declare function nextTag(): string;
|
|
62
|
+
/** Reset tag counter (for testing) */
|
|
63
|
+
export declare function resetTags(): void;
|
|
64
|
+
/** Build an IMAP command string (tag + command + CRLF) */
|
|
65
|
+
export declare function buildCommand(tag: string, command: string): string;
|
|
66
|
+
/** Build LOGIN command */
|
|
67
|
+
export declare function loginCommand(tag: string, user: string, pass: string): string;
|
|
68
|
+
/** Build AUTHENTICATE XOAUTH2 command */
|
|
69
|
+
export declare function xoauth2Command(tag: string, user: string, token: string): string;
|
|
70
|
+
/** Build LIST command */
|
|
71
|
+
export declare function listCommand(tag: string, ref?: string, pattern?: string): string;
|
|
72
|
+
/** Build SELECT command */
|
|
73
|
+
export declare function selectCommand(tag: string, mailbox: string): string;
|
|
74
|
+
/** Build EXAMINE command (read-only SELECT) */
|
|
75
|
+
export declare function examineCommand(tag: string, mailbox: string): string;
|
|
76
|
+
/** Build STATUS command */
|
|
77
|
+
export declare function statusCommand(tag: string, mailbox: string, items?: string[]): string;
|
|
78
|
+
/** Build UID FETCH command */
|
|
79
|
+
export declare function fetchCommand(tag: string, range: string, items: string[]): string;
|
|
80
|
+
/** Build UID SEARCH command */
|
|
81
|
+
export declare function searchCommand(tag: string, criteria: string): string;
|
|
82
|
+
/** Build UID STORE command (set/add/remove flags) */
|
|
83
|
+
export declare function storeCommand(tag: string, uid: number, action: string, flags: string[]): string;
|
|
84
|
+
/** Build UID COPY command */
|
|
85
|
+
export declare function copyCommand(tag: string, uid: number, destination: string): string;
|
|
86
|
+
/** Build UID MOVE command */
|
|
87
|
+
export declare function moveCommand(tag: string, uid: number, destination: string): string;
|
|
88
|
+
/** Build APPEND command header (body follows as literal) */
|
|
89
|
+
export declare function appendCommand(tag: string, mailbox: string, flags: string[], size: number): string;
|
|
90
|
+
/** Build IDLE command */
|
|
91
|
+
export declare function idleCommand(tag: string): string;
|
|
92
|
+
/** Build DONE command (ends IDLE) */
|
|
93
|
+
export declare function doneCommand(): string;
|
|
94
|
+
/** Build STARTTLS command */
|
|
95
|
+
export declare function starttlsCommand(tag: string): string;
|
|
96
|
+
/** Build LOGOUT command */
|
|
97
|
+
export declare function logoutCommand(tag: string): string;
|
|
98
|
+
/** Build CAPABILITY command */
|
|
99
|
+
export declare function capabilityCommand(tag: string): string;
|
|
100
|
+
/** Build NOOP command */
|
|
101
|
+
export declare function noopCommand(tag: string): string;
|
|
102
|
+
/** Build CREATE command */
|
|
103
|
+
export declare function createCommand(tag: string, mailbox: string): string;
|
|
104
|
+
/** Build DELETE command */
|
|
105
|
+
export declare function deleteMailboxCommand(tag: string, mailbox: string): string;
|
|
106
|
+
/** Build RENAME command */
|
|
107
|
+
export declare function renameCommand(tag: string, from: string, to: string): string;
|
|
108
|
+
/** Parse a single IMAP response line */
|
|
109
|
+
export declare function parseResponseLine(line: string): ImapResponse;
|
|
110
|
+
/** Parse a LIST response line: * LIST (\flags) "delimiter" "path" */
|
|
111
|
+
export declare function parseListResponse(text: string): ListData | null;
|
|
112
|
+
/** Parse STATUS response: * STATUS "mailbox" (MESSAGES n UIDNEXT n ...) */
|
|
113
|
+
export declare function parseStatusResponse(text: string): StatusData | null;
|
|
114
|
+
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
115
|
+
export declare function parseSearchResponse(text: string): number[];
|
|
116
|
+
/** Parse FLAGS from a FETCH or SELECT response */
|
|
117
|
+
export declare function parseFlags(flagStr: string): Set<string>;
|
|
118
|
+
/** Parse ENVELOPE from FETCH — simplified parser for the common fields */
|
|
119
|
+
export declare function parseEnvelope(envStr: string): EnvelopeData;
|
|
120
|
+
/** Build SEARCH criteria string from common search parameters */
|
|
121
|
+
export declare function buildSearchCriteria(criteria: {
|
|
122
|
+
since?: Date;
|
|
123
|
+
before?: Date;
|
|
124
|
+
from?: string;
|
|
125
|
+
to?: string;
|
|
126
|
+
subject?: string;
|
|
127
|
+
body?: string;
|
|
128
|
+
uid?: string;
|
|
129
|
+
all?: boolean;
|
|
130
|
+
}): string;
|
|
131
|
+
//# sourceMappingURL=imap-protocol.d.ts.map
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP protocol parser and command builder.
|
|
3
|
+
* Pure string logic — no I/O, no Node.js dependencies.
|
|
4
|
+
* Works in browser, Node.js, or worker thread.
|
|
5
|
+
*/
|
|
6
|
+
// ── Command Builder ──
|
|
7
|
+
let tagCounter = 0;
|
|
8
|
+
/** Generate a unique command tag */
|
|
9
|
+
export function nextTag() {
|
|
10
|
+
return `A${++tagCounter}`;
|
|
11
|
+
}
|
|
12
|
+
/** Reset tag counter (for testing) */
|
|
13
|
+
export function resetTags() {
|
|
14
|
+
tagCounter = 0;
|
|
15
|
+
}
|
|
16
|
+
/** Build an IMAP command string (tag + command + CRLF) */
|
|
17
|
+
export function buildCommand(tag, command) {
|
|
18
|
+
return `${tag} ${command}\r\n`;
|
|
19
|
+
}
|
|
20
|
+
/** Build LOGIN command */
|
|
21
|
+
export function loginCommand(tag, user, pass) {
|
|
22
|
+
// Quote user and password to handle special characters
|
|
23
|
+
return buildCommand(tag, `LOGIN ${quoteString(user)} ${quoteString(pass)}`);
|
|
24
|
+
}
|
|
25
|
+
/** Build AUTHENTICATE XOAUTH2 command */
|
|
26
|
+
export function xoauth2Command(tag, user, token) {
|
|
27
|
+
const authStr = `user=${user}\x01auth=Bearer ${token}\x01\x01`;
|
|
28
|
+
const b64 = btoa(authStr);
|
|
29
|
+
return buildCommand(tag, `AUTHENTICATE XOAUTH2 ${b64}`);
|
|
30
|
+
}
|
|
31
|
+
/** Build LIST command */
|
|
32
|
+
export function listCommand(tag, ref = '""', pattern = '"*"') {
|
|
33
|
+
return buildCommand(tag, `LIST ${ref} ${pattern}`);
|
|
34
|
+
}
|
|
35
|
+
/** Build SELECT command */
|
|
36
|
+
export function selectCommand(tag, mailbox) {
|
|
37
|
+
return buildCommand(tag, `SELECT ${quoteMailbox(mailbox)}`);
|
|
38
|
+
}
|
|
39
|
+
/** Build EXAMINE command (read-only SELECT) */
|
|
40
|
+
export function examineCommand(tag, mailbox) {
|
|
41
|
+
return buildCommand(tag, `EXAMINE ${quoteMailbox(mailbox)}`);
|
|
42
|
+
}
|
|
43
|
+
/** Build STATUS command */
|
|
44
|
+
export function statusCommand(tag, mailbox, items = ["MESSAGES", "UIDNEXT"]) {
|
|
45
|
+
return buildCommand(tag, `STATUS ${quoteMailbox(mailbox)} (${items.join(" ")})`);
|
|
46
|
+
}
|
|
47
|
+
/** Build UID FETCH command */
|
|
48
|
+
export function fetchCommand(tag, range, items) {
|
|
49
|
+
return buildCommand(tag, `UID FETCH ${range} (${items.join(" ")})`);
|
|
50
|
+
}
|
|
51
|
+
/** Build UID SEARCH command */
|
|
52
|
+
export function searchCommand(tag, criteria) {
|
|
53
|
+
return buildCommand(tag, `UID SEARCH ${criteria}`);
|
|
54
|
+
}
|
|
55
|
+
/** Build UID STORE command (set/add/remove flags) */
|
|
56
|
+
export function storeCommand(tag, uid, action, flags) {
|
|
57
|
+
return buildCommand(tag, `UID STORE ${uid} ${action} (${flags.join(" ")})`);
|
|
58
|
+
}
|
|
59
|
+
/** Build UID COPY command */
|
|
60
|
+
export function copyCommand(tag, uid, destination) {
|
|
61
|
+
return buildCommand(tag, `UID COPY ${uid} ${quoteMailbox(destination)}`);
|
|
62
|
+
}
|
|
63
|
+
/** Build UID MOVE command */
|
|
64
|
+
export function moveCommand(tag, uid, destination) {
|
|
65
|
+
return buildCommand(tag, `UID MOVE ${uid} ${quoteMailbox(destination)}`);
|
|
66
|
+
}
|
|
67
|
+
/** Build APPEND command header (body follows as literal) */
|
|
68
|
+
export function appendCommand(tag, mailbox, flags, size) {
|
|
69
|
+
const flagStr = flags.length > 0 ? ` (${flags.join(" ")})` : "";
|
|
70
|
+
return buildCommand(tag, `APPEND ${quoteMailbox(mailbox)}${flagStr} {${size}}`);
|
|
71
|
+
}
|
|
72
|
+
/** Build IDLE command */
|
|
73
|
+
export function idleCommand(tag) {
|
|
74
|
+
return buildCommand(tag, "IDLE");
|
|
75
|
+
}
|
|
76
|
+
/** Build DONE command (ends IDLE) */
|
|
77
|
+
export function doneCommand() {
|
|
78
|
+
return "DONE\r\n";
|
|
79
|
+
}
|
|
80
|
+
/** Build STARTTLS command */
|
|
81
|
+
export function starttlsCommand(tag) {
|
|
82
|
+
return buildCommand(tag, "STARTTLS");
|
|
83
|
+
}
|
|
84
|
+
/** Build LOGOUT command */
|
|
85
|
+
export function logoutCommand(tag) {
|
|
86
|
+
return buildCommand(tag, "LOGOUT");
|
|
87
|
+
}
|
|
88
|
+
/** Build CAPABILITY command */
|
|
89
|
+
export function capabilityCommand(tag) {
|
|
90
|
+
return buildCommand(tag, "CAPABILITY");
|
|
91
|
+
}
|
|
92
|
+
/** Build NOOP command */
|
|
93
|
+
export function noopCommand(tag) {
|
|
94
|
+
return buildCommand(tag, "NOOP");
|
|
95
|
+
}
|
|
96
|
+
/** Build CREATE command */
|
|
97
|
+
export function createCommand(tag, mailbox) {
|
|
98
|
+
return buildCommand(tag, `CREATE ${quoteMailbox(mailbox)}`);
|
|
99
|
+
}
|
|
100
|
+
/** Build DELETE command */
|
|
101
|
+
export function deleteMailboxCommand(tag, mailbox) {
|
|
102
|
+
return buildCommand(tag, `DELETE ${quoteMailbox(mailbox)}`);
|
|
103
|
+
}
|
|
104
|
+
/** Build RENAME command */
|
|
105
|
+
export function renameCommand(tag, from, to) {
|
|
106
|
+
return buildCommand(tag, `RENAME ${quoteMailbox(from)} ${quoteMailbox(to)}`);
|
|
107
|
+
}
|
|
108
|
+
// ── Response Parser ──
|
|
109
|
+
/** Parse a single IMAP response line */
|
|
110
|
+
export function parseResponseLine(line) {
|
|
111
|
+
const trimmed = line.replace(/\r?\n$/, "");
|
|
112
|
+
// Continuation response
|
|
113
|
+
if (trimmed.startsWith("+ ") || trimmed === "+") {
|
|
114
|
+
return { tag: "+", type: "CONTINUE", text: trimmed.substring(2), raw: trimmed };
|
|
115
|
+
}
|
|
116
|
+
// Untagged response
|
|
117
|
+
if (trimmed.startsWith("* ")) {
|
|
118
|
+
const rest = trimmed.substring(2);
|
|
119
|
+
// Check for numeric response (e.g., "* 5 EXISTS")
|
|
120
|
+
const numMatch = rest.match(/^(\d+)\s+(\S+)(.*)$/);
|
|
121
|
+
if (numMatch) {
|
|
122
|
+
return { tag: "*", type: numMatch[2].toUpperCase(), text: rest, raw: trimmed };
|
|
123
|
+
}
|
|
124
|
+
// Status/capability/list response
|
|
125
|
+
const spaceIdx = rest.indexOf(" ");
|
|
126
|
+
if (spaceIdx > 0) {
|
|
127
|
+
return { tag: "*", type: rest.substring(0, spaceIdx).toUpperCase(), text: rest.substring(spaceIdx + 1), raw: trimmed };
|
|
128
|
+
}
|
|
129
|
+
return { tag: "*", type: rest.toUpperCase(), text: "", raw: trimmed };
|
|
130
|
+
}
|
|
131
|
+
// Tagged response (e.g., "A1 OK Login completed")
|
|
132
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
133
|
+
if (spaceIdx > 0) {
|
|
134
|
+
const tag = trimmed.substring(0, spaceIdx);
|
|
135
|
+
const rest = trimmed.substring(spaceIdx + 1);
|
|
136
|
+
const spaceIdx2 = rest.indexOf(" ");
|
|
137
|
+
if (spaceIdx2 > 0) {
|
|
138
|
+
return { tag, type: rest.substring(0, spaceIdx2).toUpperCase(), text: rest.substring(spaceIdx2 + 1), raw: trimmed };
|
|
139
|
+
}
|
|
140
|
+
return { tag, type: rest.toUpperCase(), text: "", raw: trimmed };
|
|
141
|
+
}
|
|
142
|
+
return { tag: "", type: "UNKNOWN", text: trimmed, raw: trimmed };
|
|
143
|
+
}
|
|
144
|
+
/** Parse a LIST response line: * LIST (\flags) "delimiter" "path" */
|
|
145
|
+
export function parseListResponse(text) {
|
|
146
|
+
// Match: LIST (flags) "delimiter" "path" or LIST (flags) "delimiter" path
|
|
147
|
+
const match = text.match(/^LIST\s+\(([^)]*)\)\s+"([^"]*)"\s+(?:"([^"]+)"|(\S+))$/i);
|
|
148
|
+
if (!match)
|
|
149
|
+
return null;
|
|
150
|
+
const flags = match[1] ? match[1].split(/\s+/).filter(Boolean) : [];
|
|
151
|
+
const delimiter = match[2] || ".";
|
|
152
|
+
const path = match[3] || match[4] || "";
|
|
153
|
+
return { flags, delimiter, path };
|
|
154
|
+
}
|
|
155
|
+
/** Parse STATUS response: * STATUS "mailbox" (MESSAGES n UIDNEXT n ...) */
|
|
156
|
+
export function parseStatusResponse(text) {
|
|
157
|
+
const match = text.match(/\(([^)]+)\)/);
|
|
158
|
+
if (!match)
|
|
159
|
+
return null;
|
|
160
|
+
const pairs = match[1].split(/\s+/);
|
|
161
|
+
const data = {};
|
|
162
|
+
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
163
|
+
const key = pairs[i].toUpperCase();
|
|
164
|
+
const val = parseInt(pairs[i + 1]);
|
|
165
|
+
if (key === "MESSAGES")
|
|
166
|
+
data.messages = val;
|
|
167
|
+
else if (key === "RECENT")
|
|
168
|
+
data.recent = val;
|
|
169
|
+
else if (key === "UIDNEXT")
|
|
170
|
+
data.uidNext = val;
|
|
171
|
+
else if (key === "UIDVALIDITY")
|
|
172
|
+
data.uidValidity = val;
|
|
173
|
+
else if (key === "UNSEEN")
|
|
174
|
+
data.unseen = val;
|
|
175
|
+
}
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
179
|
+
export function parseSearchResponse(text) {
|
|
180
|
+
if (!text.trim())
|
|
181
|
+
return [];
|
|
182
|
+
return text.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
183
|
+
}
|
|
184
|
+
/** Parse FLAGS from a FETCH or SELECT response */
|
|
185
|
+
export function parseFlags(flagStr) {
|
|
186
|
+
const match = flagStr.match(/\(([^)]*)\)/);
|
|
187
|
+
if (!match)
|
|
188
|
+
return new Set();
|
|
189
|
+
return new Set(match[1].split(/\s+/).filter(Boolean));
|
|
190
|
+
}
|
|
191
|
+
/** Parse ENVELOPE from FETCH — simplified parser for the common fields */
|
|
192
|
+
export function parseEnvelope(envStr) {
|
|
193
|
+
// ENVELOPE is a parenthesized list — this is a simplified parser
|
|
194
|
+
// Full RFC 3501 envelope: (date subject from sender reply-to to cc bcc in-reply-to message-id)
|
|
195
|
+
const result = {
|
|
196
|
+
date: null, subject: "", from: [], sender: [], replyTo: [],
|
|
197
|
+
to: [], cc: [], bcc: [], inReplyTo: "", messageId: ""
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
const tokens = tokenizeParenList(envStr);
|
|
201
|
+
if (tokens.length >= 10) {
|
|
202
|
+
result.date = tokens[0] !== "NIL" ? new Date(unquote(tokens[0])) : null;
|
|
203
|
+
result.subject = decodeImapString(unquote(tokens[1]));
|
|
204
|
+
result.from = parseAddressList(tokens[2]);
|
|
205
|
+
result.sender = parseAddressList(tokens[3]);
|
|
206
|
+
result.replyTo = parseAddressList(tokens[4]);
|
|
207
|
+
result.to = parseAddressList(tokens[5]);
|
|
208
|
+
result.cc = parseAddressList(tokens[6]);
|
|
209
|
+
result.bcc = parseAddressList(tokens[7]);
|
|
210
|
+
result.inReplyTo = unquote(tokens[8]);
|
|
211
|
+
result.messageId = unquote(tokens[9]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Parsing envelope is best-effort — incomplete data is acceptable
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
// ── IMAP String Utilities ──
|
|
220
|
+
/** Quote a string for IMAP (handles special characters) */
|
|
221
|
+
function quoteString(s) {
|
|
222
|
+
if (/[\x00-\x1f\x7f"\\]/.test(s)) {
|
|
223
|
+
// Use literal for problematic characters
|
|
224
|
+
return `{${Buffer.byteLength(s)}}\r\n${s}`;
|
|
225
|
+
}
|
|
226
|
+
return `"${s}"`;
|
|
227
|
+
}
|
|
228
|
+
/** Quote a mailbox name */
|
|
229
|
+
function quoteMailbox(name) {
|
|
230
|
+
if (name === "INBOX")
|
|
231
|
+
return name;
|
|
232
|
+
if (/[\s"\\*%{]/.test(name))
|
|
233
|
+
return `"${name.replace(/["\\]/g, "\\$&")}"`;
|
|
234
|
+
return name;
|
|
235
|
+
}
|
|
236
|
+
/** Remove quotes from an IMAP string */
|
|
237
|
+
function unquote(s) {
|
|
238
|
+
if (!s || s === "NIL")
|
|
239
|
+
return "";
|
|
240
|
+
if (s.startsWith('"') && s.endsWith('"'))
|
|
241
|
+
return s.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
242
|
+
return s;
|
|
243
|
+
}
|
|
244
|
+
/** Decode IMAP encoded-word (=?charset?encoding?text?=) */
|
|
245
|
+
function decodeImapString(s) {
|
|
246
|
+
if (!s)
|
|
247
|
+
return "";
|
|
248
|
+
return s.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_match, charset, encoding, text) => {
|
|
249
|
+
try {
|
|
250
|
+
if (encoding.toUpperCase() === "B") {
|
|
251
|
+
return Buffer.from(text, "base64").toString("utf-8");
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Quoted-printable
|
|
255
|
+
return text.replace(/=([0-9A-F]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/_/g, " ");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return text;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** Tokenize a parenthesized IMAP list (top-level only) */
|
|
264
|
+
function tokenizeParenList(s) {
|
|
265
|
+
const tokens = [];
|
|
266
|
+
let i = 0;
|
|
267
|
+
const str = s.trim();
|
|
268
|
+
// Skip outer parens
|
|
269
|
+
const start = str.startsWith("(") ? 1 : 0;
|
|
270
|
+
const end = str.endsWith(")") ? str.length - 1 : str.length;
|
|
271
|
+
i = start;
|
|
272
|
+
while (i < end) {
|
|
273
|
+
// Skip whitespace
|
|
274
|
+
while (i < end && str[i] === " ")
|
|
275
|
+
i++;
|
|
276
|
+
if (i >= end)
|
|
277
|
+
break;
|
|
278
|
+
if (str[i] === "(") {
|
|
279
|
+
// Nested paren group — find matching close
|
|
280
|
+
let depth = 1;
|
|
281
|
+
let j = i + 1;
|
|
282
|
+
while (j < end && depth > 0) {
|
|
283
|
+
if (str[j] === "(")
|
|
284
|
+
depth++;
|
|
285
|
+
else if (str[j] === ")")
|
|
286
|
+
depth--;
|
|
287
|
+
j++;
|
|
288
|
+
}
|
|
289
|
+
tokens.push(str.substring(i, j));
|
|
290
|
+
i = j;
|
|
291
|
+
}
|
|
292
|
+
else if (str[i] === '"') {
|
|
293
|
+
// Quoted string
|
|
294
|
+
let j = i + 1;
|
|
295
|
+
while (j < end) {
|
|
296
|
+
if (str[j] === "\\" && j + 1 < end) {
|
|
297
|
+
j += 2;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (str[j] === '"') {
|
|
301
|
+
j++;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
j++;
|
|
305
|
+
}
|
|
306
|
+
tokens.push(str.substring(i, j));
|
|
307
|
+
i = j;
|
|
308
|
+
}
|
|
309
|
+
else if (str.substring(i, i + 3).toUpperCase() === "NIL") {
|
|
310
|
+
tokens.push("NIL");
|
|
311
|
+
i += 3;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// Atom
|
|
315
|
+
let j = i;
|
|
316
|
+
while (j < end && str[j] !== " " && str[j] !== ")" && str[j] !== "(")
|
|
317
|
+
j++;
|
|
318
|
+
tokens.push(str.substring(i, j));
|
|
319
|
+
i = j;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return tokens;
|
|
323
|
+
}
|
|
324
|
+
/** Parse an IMAP address list: ((name NIL mailbox host) ...) */
|
|
325
|
+
function parseAddressList(token) {
|
|
326
|
+
if (!token || token === "NIL")
|
|
327
|
+
return [];
|
|
328
|
+
const addrs = [];
|
|
329
|
+
// Match each (name atdomain mailbox host) group
|
|
330
|
+
const re = /\(([^)]*)\)/g;
|
|
331
|
+
let m;
|
|
332
|
+
while ((m = re.exec(token)) !== null) {
|
|
333
|
+
const parts = tokenizeParenList(m[1]);
|
|
334
|
+
if (parts.length >= 4) {
|
|
335
|
+
const name = decodeImapString(unquote(parts[0]));
|
|
336
|
+
const mailbox = unquote(parts[2]);
|
|
337
|
+
const host = unquote(parts[3]);
|
|
338
|
+
const address = mailbox && host ? `${mailbox}@${host}` : mailbox || "";
|
|
339
|
+
addrs.push({ name, address });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return addrs;
|
|
343
|
+
}
|
|
344
|
+
/** Build SEARCH criteria string from common search parameters */
|
|
345
|
+
export function buildSearchCriteria(criteria) {
|
|
346
|
+
const parts = [];
|
|
347
|
+
if (criteria.all)
|
|
348
|
+
parts.push("ALL");
|
|
349
|
+
if (criteria.since)
|
|
350
|
+
parts.push(`SINCE ${formatImapDate(criteria.since)}`);
|
|
351
|
+
if (criteria.before)
|
|
352
|
+
parts.push(`BEFORE ${formatImapDate(criteria.before)}`);
|
|
353
|
+
if (criteria.from)
|
|
354
|
+
parts.push(`FROM "${criteria.from}"`);
|
|
355
|
+
if (criteria.to)
|
|
356
|
+
parts.push(`TO "${criteria.to}"`);
|
|
357
|
+
if (criteria.subject)
|
|
358
|
+
parts.push(`SUBJECT "${criteria.subject}"`);
|
|
359
|
+
if (criteria.body)
|
|
360
|
+
parts.push(`BODY "${criteria.body}"`);
|
|
361
|
+
if (criteria.uid)
|
|
362
|
+
parts.push(`UID ${criteria.uid}`);
|
|
363
|
+
return parts.length > 0 ? parts.join(" ") : "ALL";
|
|
364
|
+
}
|
|
365
|
+
/** Format a date for IMAP (DD-Mon-YYYY) */
|
|
366
|
+
function formatImapDate(d) {
|
|
367
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
368
|
+
return `${d.getDate()}-${months[d.getMonth()]}-${d.getFullYear()}`;
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=imap-protocol.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js transport for IMAP — uses net/tls modules.
|
|
3
|
+
* This is the desktop implementation.
|
|
4
|
+
*/
|
|
5
|
+
import type { ImapTransport } from "./transport.js";
|
|
6
|
+
export declare class NodeTransport implements ImapTransport {
|
|
7
|
+
private socket;
|
|
8
|
+
private dataHandler;
|
|
9
|
+
private closeHandler;
|
|
10
|
+
private errorHandler;
|
|
11
|
+
private _connected;
|
|
12
|
+
private rejectUnauthorized;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
rejectUnauthorized?: boolean;
|
|
15
|
+
});
|
|
16
|
+
get connected(): boolean;
|
|
17
|
+
connect(host: string, port: number, useTls: boolean, servername?: string): Promise<void>;
|
|
18
|
+
upgradeTLS(servername?: string): Promise<void>;
|
|
19
|
+
write(data: string | Uint8Array): Promise<void>;
|
|
20
|
+
onData(handler: (data: string) => void): void;
|
|
21
|
+
onClose(handler: (hadError: boolean) => void): void;
|
|
22
|
+
onError(handler: (err: Error) => void): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=node-transport.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js transport for IMAP — uses net/tls modules.
|
|
3
|
+
* This is the desktop implementation.
|
|
4
|
+
*/
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import * as tls from "node:tls";
|
|
7
|
+
export class NodeTransport {
|
|
8
|
+
socket = null;
|
|
9
|
+
dataHandler = null;
|
|
10
|
+
closeHandler = null;
|
|
11
|
+
errorHandler = null;
|
|
12
|
+
_connected = false;
|
|
13
|
+
rejectUnauthorized;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.rejectUnauthorized = options?.rejectUnauthorized ?? true;
|
|
16
|
+
}
|
|
17
|
+
get connected() { return this._connected; }
|
|
18
|
+
async connect(host, port, useTls, servername) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const onConnect = () => {
|
|
21
|
+
this._connected = true;
|
|
22
|
+
resolve();
|
|
23
|
+
};
|
|
24
|
+
const onError = (err) => {
|
|
25
|
+
this._connected = false;
|
|
26
|
+
reject(err);
|
|
27
|
+
};
|
|
28
|
+
if (useTls) {
|
|
29
|
+
this.socket = tls.connect({
|
|
30
|
+
host, port,
|
|
31
|
+
servername: servername || host,
|
|
32
|
+
rejectUnauthorized: this.rejectUnauthorized,
|
|
33
|
+
}, onConnect);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.socket = net.connect({ host, port }, onConnect);
|
|
37
|
+
}
|
|
38
|
+
this.socket.once("error", onError);
|
|
39
|
+
this.socket.setKeepAlive(true, 5000);
|
|
40
|
+
// Wire up persistent handlers after connect
|
|
41
|
+
this.socket.on("data", (chunk) => {
|
|
42
|
+
if (this.dataHandler)
|
|
43
|
+
this.dataHandler(chunk.toString("utf-8"));
|
|
44
|
+
});
|
|
45
|
+
this.socket.on("close", (hadError) => {
|
|
46
|
+
this._connected = false;
|
|
47
|
+
if (this.closeHandler)
|
|
48
|
+
this.closeHandler(hadError);
|
|
49
|
+
});
|
|
50
|
+
this.socket.on("error", (err) => {
|
|
51
|
+
if (this.errorHandler)
|
|
52
|
+
this.errorHandler(err);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async upgradeTLS(servername) {
|
|
57
|
+
if (!this.socket)
|
|
58
|
+
throw new Error("Not connected");
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const plainSocket = this.socket;
|
|
61
|
+
const tlsSocket = tls.connect({
|
|
62
|
+
socket: plainSocket,
|
|
63
|
+
servername: servername || undefined,
|
|
64
|
+
rejectUnauthorized: this.rejectUnauthorized,
|
|
65
|
+
}, () => {
|
|
66
|
+
this.socket = tlsSocket;
|
|
67
|
+
// Re-wire data handler to new socket
|
|
68
|
+
tlsSocket.on("data", (chunk) => {
|
|
69
|
+
if (this.dataHandler)
|
|
70
|
+
this.dataHandler(chunk.toString("utf-8"));
|
|
71
|
+
});
|
|
72
|
+
tlsSocket.on("close", (hadError) => {
|
|
73
|
+
this._connected = false;
|
|
74
|
+
if (this.closeHandler)
|
|
75
|
+
this.closeHandler(hadError);
|
|
76
|
+
});
|
|
77
|
+
tlsSocket.on("error", (err) => {
|
|
78
|
+
if (this.errorHandler)
|
|
79
|
+
this.errorHandler(err);
|
|
80
|
+
});
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
tlsSocket.once("error", reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async write(data) {
|
|
87
|
+
if (!this.socket || this.socket.destroyed)
|
|
88
|
+
throw new Error("Not connected");
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
this.socket.write(data, (err) => {
|
|
91
|
+
if (err)
|
|
92
|
+
reject(err);
|
|
93
|
+
else
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
onData(handler) { this.dataHandler = handler; }
|
|
99
|
+
onClose(handler) { this.closeHandler = handler; }
|
|
100
|
+
onError(handler) { this.errorHandler = handler; }
|
|
101
|
+
close() {
|
|
102
|
+
this._connected = false;
|
|
103
|
+
if (this.socket && !this.socket.destroyed) {
|
|
104
|
+
this.socket.destroy();
|
|
105
|
+
}
|
|
106
|
+
this.socket = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=node-transport.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract transport interface for IMAP connections.
|
|
3
|
+
* Implementations: NodeTransport (desktop), BridgeTransport (Android/WebView)
|
|
4
|
+
*/
|
|
5
|
+
export interface ImapTransport {
|
|
6
|
+
/** Connect to host:port. If tls=true, connect with TLS directly (port 993). */
|
|
7
|
+
connect(host: string, port: number, tls: boolean, servername?: string): Promise<void>;
|
|
8
|
+
/** Upgrade an existing plaintext connection to TLS (STARTTLS). */
|
|
9
|
+
upgradeTLS(servername?: string): Promise<void>;
|
|
10
|
+
/** Write data to the connection. */
|
|
11
|
+
write(data: string | Uint8Array): Promise<void>;
|
|
12
|
+
/** Register a handler for incoming data. */
|
|
13
|
+
onData(handler: (data: string) => void): void;
|
|
14
|
+
/** Register a handler for connection close. */
|
|
15
|
+
onClose(handler: (hadError: boolean) => void): void;
|
|
16
|
+
/** Register a handler for errors. */
|
|
17
|
+
onError(handler: (err: Error) => void): void;
|
|
18
|
+
/** Close the connection. */
|
|
19
|
+
close(): void;
|
|
20
|
+
/** Whether the connection is active. */
|
|
21
|
+
readonly connected: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Factory function type for creating transports */
|
|
24
|
+
export type TransportFactory = () => ImapTransport;
|
|
25
|
+
//# sourceMappingURL=transport.d.ts.map
|
package/index.d.ts
CHANGED
|
@@ -4,4 +4,10 @@ export type { SearchObject } from 'imapflow';
|
|
|
4
4
|
export { FetchedMessage as FetchedMessageClass } from './imaplib/types.js';
|
|
5
5
|
export { createAutoImapConfig } from './imaplib/gmail.js';
|
|
6
6
|
export { isGmailUser, isGmailServer, createImapConfig, type OAuthImapConfig } from './imaplib/gmail.js';
|
|
7
|
+
export { NativeImapClient } from './imaplib/imap-native.js';
|
|
8
|
+
export type { NativeFetchedMessage, NativeFolder, MailboxInfo } from './imaplib/imap-native.js';
|
|
9
|
+
export type { ImapTransport, TransportFactory } from './imaplib/transport.js';
|
|
10
|
+
export { NodeTransport } from './imaplib/node-transport.js';
|
|
11
|
+
export { CompatImapClient } from './imaplib/imap-compat.js';
|
|
12
|
+
export type { SpecialFolders } from './imaplib/imap-compat.js';
|
|
7
13
|
//# sourceMappingURL=index.d.ts.map
|