@bobfrankston/iflow-direct 0.1.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/bridge-transport.d.ts +30 -0
- package/bridge-transport.js +60 -0
- package/credentials.json +9 -0
- package/gmail.d.ts +39 -0
- package/gmail.js +66 -0
- package/imap-compat.d.ts +83 -0
- package/imap-compat.js +254 -0
- package/imap-native.d.ts +135 -0
- package/imap-native.js +733 -0
- package/imap-protocol.d.ts +133 -0
- package/imap-protocol.js +391 -0
- package/index.d.ts +18 -0
- package/index.js +18 -0
- package/package.json +42 -0
- package/transport.d.ts +25 -0
- package/transport.js +6 -0
- package/types.d.ts +14 -0
- package/types.js +5 -0
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
/** Literal data keyed by BODY section name (e.g. "BODY[]", "BODY[HEADER]") */
|
|
17
|
+
literals?: Map<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/** Parsed FETCH response data */
|
|
20
|
+
export interface FetchData {
|
|
21
|
+
seq: number;
|
|
22
|
+
uid?: number;
|
|
23
|
+
flags?: Set<string>;
|
|
24
|
+
internalDate?: Date;
|
|
25
|
+
size?: number;
|
|
26
|
+
envelope?: EnvelopeData;
|
|
27
|
+
bodyStructure?: any;
|
|
28
|
+
headers?: string;
|
|
29
|
+
source?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Parsed ENVELOPE data */
|
|
32
|
+
export interface EnvelopeData {
|
|
33
|
+
date: Date | null;
|
|
34
|
+
subject: string;
|
|
35
|
+
from: AddressData[];
|
|
36
|
+
sender: AddressData[];
|
|
37
|
+
replyTo: AddressData[];
|
|
38
|
+
to: AddressData[];
|
|
39
|
+
cc: AddressData[];
|
|
40
|
+
bcc: AddressData[];
|
|
41
|
+
inReplyTo: string;
|
|
42
|
+
messageId: string;
|
|
43
|
+
}
|
|
44
|
+
export interface AddressData {
|
|
45
|
+
name: string;
|
|
46
|
+
address: string;
|
|
47
|
+
}
|
|
48
|
+
/** Parsed folder LIST data */
|
|
49
|
+
export interface ListData {
|
|
50
|
+
flags: string[];
|
|
51
|
+
delimiter: string;
|
|
52
|
+
path: string;
|
|
53
|
+
}
|
|
54
|
+
/** Parsed STATUS data */
|
|
55
|
+
export interface StatusData {
|
|
56
|
+
messages?: number;
|
|
57
|
+
recent?: number;
|
|
58
|
+
uidNext?: number;
|
|
59
|
+
uidValidity?: number;
|
|
60
|
+
unseen?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Generate a unique command tag */
|
|
63
|
+
export declare function nextTag(): string;
|
|
64
|
+
/** Reset tag counter (for testing) */
|
|
65
|
+
export declare function resetTags(): void;
|
|
66
|
+
/** Build an IMAP command string (tag + command + CRLF) */
|
|
67
|
+
export declare function buildCommand(tag: string, command: string): string;
|
|
68
|
+
/** Build LOGIN command */
|
|
69
|
+
export declare function loginCommand(tag: string, user: string, pass: string): string;
|
|
70
|
+
/** Build AUTHENTICATE XOAUTH2 command */
|
|
71
|
+
export declare function xoauth2Command(tag: string, user: string, token: string): string;
|
|
72
|
+
/** Build LIST command */
|
|
73
|
+
export declare function listCommand(tag: string, ref?: string, pattern?: string): string;
|
|
74
|
+
/** Build SELECT command */
|
|
75
|
+
export declare function selectCommand(tag: string, mailbox: string): string;
|
|
76
|
+
/** Build EXAMINE command (read-only SELECT) */
|
|
77
|
+
export declare function examineCommand(tag: string, mailbox: string): string;
|
|
78
|
+
/** Build STATUS command */
|
|
79
|
+
export declare function statusCommand(tag: string, mailbox: string, items?: string[]): string;
|
|
80
|
+
/** Build UID FETCH command */
|
|
81
|
+
export declare function fetchCommand(tag: string, range: string, items: string[]): string;
|
|
82
|
+
/** Build UID SEARCH command */
|
|
83
|
+
export declare function searchCommand(tag: string, criteria: string): string;
|
|
84
|
+
/** Build UID STORE command (set/add/remove flags) */
|
|
85
|
+
export declare function storeCommand(tag: string, uid: number, action: string, flags: string[]): string;
|
|
86
|
+
/** Build UID COPY command */
|
|
87
|
+
export declare function copyCommand(tag: string, uid: number, destination: string): string;
|
|
88
|
+
/** Build UID MOVE command */
|
|
89
|
+
export declare function moveCommand(tag: string, uid: number, destination: string): string;
|
|
90
|
+
/** Build APPEND command header (body follows as literal) */
|
|
91
|
+
export declare function appendCommand(tag: string, mailbox: string, flags: string[], size: number): string;
|
|
92
|
+
/** Build IDLE command */
|
|
93
|
+
export declare function idleCommand(tag: string): string;
|
|
94
|
+
/** Build DONE command (ends IDLE) */
|
|
95
|
+
export declare function doneCommand(): string;
|
|
96
|
+
/** Build STARTTLS command */
|
|
97
|
+
export declare function starttlsCommand(tag: string): string;
|
|
98
|
+
/** Build LOGOUT command */
|
|
99
|
+
export declare function logoutCommand(tag: string): string;
|
|
100
|
+
/** Build CAPABILITY command */
|
|
101
|
+
export declare function capabilityCommand(tag: string): string;
|
|
102
|
+
/** Build NOOP command */
|
|
103
|
+
export declare function noopCommand(tag: string): string;
|
|
104
|
+
/** Build CREATE command */
|
|
105
|
+
export declare function createCommand(tag: string, mailbox: string): string;
|
|
106
|
+
/** Build DELETE command */
|
|
107
|
+
export declare function deleteMailboxCommand(tag: string, mailbox: string): string;
|
|
108
|
+
/** Build RENAME command */
|
|
109
|
+
export declare function renameCommand(tag: string, from: string, to: string): string;
|
|
110
|
+
/** Parse a single IMAP response line */
|
|
111
|
+
export declare function parseResponseLine(line: string): ImapResponse;
|
|
112
|
+
/** Parse a LIST response line: * LIST (\flags) "delimiter" "path" */
|
|
113
|
+
export declare function parseListResponse(text: string): ListData | null;
|
|
114
|
+
/** Parse STATUS response: * STATUS "mailbox" (MESSAGES n UIDNEXT n ...) */
|
|
115
|
+
export declare function parseStatusResponse(text: string): StatusData | null;
|
|
116
|
+
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
117
|
+
export declare function parseSearchResponse(text: string): number[];
|
|
118
|
+
/** Parse FLAGS from a FETCH or SELECT response */
|
|
119
|
+
export declare function parseFlags(flagStr: string): Set<string>;
|
|
120
|
+
/** Parse ENVELOPE from FETCH — simplified parser for the common fields */
|
|
121
|
+
export declare function parseEnvelope(envStr: string): EnvelopeData;
|
|
122
|
+
/** Build SEARCH criteria string from common search parameters */
|
|
123
|
+
export declare function buildSearchCriteria(criteria: {
|
|
124
|
+
since?: Date;
|
|
125
|
+
before?: Date;
|
|
126
|
+
from?: string;
|
|
127
|
+
to?: string;
|
|
128
|
+
subject?: string;
|
|
129
|
+
body?: string;
|
|
130
|
+
uid?: string;
|
|
131
|
+
all?: boolean;
|
|
132
|
+
}): string;
|
|
133
|
+
//# sourceMappingURL=imap-protocol.d.ts.map
|
package/imap-protocol.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
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", "* 131441 FETCH (...)")
|
|
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: (flags) "delimiter" "path" or (flags) "delimiter" path
|
|
147
|
+
// The "LIST" prefix may or may not be present (parseResponseLine may strip it into r.type)
|
|
148
|
+
const match = text.match(/^(?:LIST\s+)?\(([^)]*)\)\s+"([^"]*)"\s+(?:"([^"]+)"|(\S+))/i);
|
|
149
|
+
if (!match)
|
|
150
|
+
return null;
|
|
151
|
+
const flags = match[1] ? match[1].split(/\s+/).filter(Boolean) : [];
|
|
152
|
+
const delimiter = match[2] || ".";
|
|
153
|
+
const path = (match[3] || match[4] || "").replace(/\r?\n$/, "");
|
|
154
|
+
return { flags, delimiter, path };
|
|
155
|
+
}
|
|
156
|
+
/** Parse STATUS response: * STATUS "mailbox" (MESSAGES n UIDNEXT n ...) */
|
|
157
|
+
export function parseStatusResponse(text) {
|
|
158
|
+
const match = text.match(/\(([^)]+)\)/);
|
|
159
|
+
if (!match)
|
|
160
|
+
return null;
|
|
161
|
+
const pairs = match[1].split(/\s+/);
|
|
162
|
+
const data = {};
|
|
163
|
+
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
164
|
+
const key = pairs[i].toUpperCase();
|
|
165
|
+
const val = parseInt(pairs[i + 1]);
|
|
166
|
+
if (key === "MESSAGES")
|
|
167
|
+
data.messages = val;
|
|
168
|
+
else if (key === "RECENT")
|
|
169
|
+
data.recent = val;
|
|
170
|
+
else if (key === "UIDNEXT")
|
|
171
|
+
data.uidNext = val;
|
|
172
|
+
else if (key === "UIDVALIDITY")
|
|
173
|
+
data.uidValidity = val;
|
|
174
|
+
else if (key === "UNSEEN")
|
|
175
|
+
data.unseen = val;
|
|
176
|
+
}
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
/** Parse UID SEARCH response: * SEARCH 1 2 3 4 5 */
|
|
180
|
+
export function parseSearchResponse(text) {
|
|
181
|
+
if (!text.trim())
|
|
182
|
+
return [];
|
|
183
|
+
return text.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
184
|
+
}
|
|
185
|
+
/** Parse FLAGS from a FETCH or SELECT response */
|
|
186
|
+
export function parseFlags(flagStr) {
|
|
187
|
+
const match = flagStr.match(/\(([^)]*)\)/);
|
|
188
|
+
if (!match)
|
|
189
|
+
return new Set();
|
|
190
|
+
return new Set(match[1].split(/\s+/).filter(Boolean));
|
|
191
|
+
}
|
|
192
|
+
/** Parse ENVELOPE from FETCH — simplified parser for the common fields */
|
|
193
|
+
export function parseEnvelope(envStr) {
|
|
194
|
+
// ENVELOPE is a parenthesized list — this is a simplified parser
|
|
195
|
+
// Full RFC 3501 envelope: (date subject from sender reply-to to cc bcc in-reply-to message-id)
|
|
196
|
+
const result = {
|
|
197
|
+
date: null, subject: "", from: [], sender: [], replyTo: [],
|
|
198
|
+
to: [], cc: [], bcc: [], inReplyTo: "", messageId: ""
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
const tokens = tokenizeParenList(envStr);
|
|
202
|
+
if (tokens.length >= 10) {
|
|
203
|
+
result.date = tokens[0] !== "NIL" ? new Date(unquote(tokens[0])) : null;
|
|
204
|
+
result.subject = decodeImapString(unquote(tokens[1]));
|
|
205
|
+
result.from = parseAddressList(tokens[2]);
|
|
206
|
+
result.sender = parseAddressList(tokens[3]);
|
|
207
|
+
result.replyTo = parseAddressList(tokens[4]);
|
|
208
|
+
result.to = parseAddressList(tokens[5]);
|
|
209
|
+
result.cc = parseAddressList(tokens[6]);
|
|
210
|
+
result.bcc = parseAddressList(tokens[7]);
|
|
211
|
+
result.inReplyTo = unquote(tokens[8]);
|
|
212
|
+
result.messageId = unquote(tokens[9]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Parsing envelope is best-effort — incomplete data is acceptable
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
// ── IMAP String Utilities ──
|
|
221
|
+
/** Quote a string for IMAP (handles special characters) */
|
|
222
|
+
function quoteString(s) {
|
|
223
|
+
if (/[\x00-\x1f\x7f"\\]/.test(s)) {
|
|
224
|
+
// Use literal for problematic characters
|
|
225
|
+
return `{${new TextEncoder().encode(s).length}}\r\n${s}`;
|
|
226
|
+
}
|
|
227
|
+
return `"${s}"`;
|
|
228
|
+
}
|
|
229
|
+
/** Quote a mailbox name */
|
|
230
|
+
function quoteMailbox(name) {
|
|
231
|
+
if (name === "INBOX")
|
|
232
|
+
return name;
|
|
233
|
+
if (/[\s"\\*%{]/.test(name))
|
|
234
|
+
return `"${name.replace(/["\\]/g, "\\$&")}"`;
|
|
235
|
+
return name;
|
|
236
|
+
}
|
|
237
|
+
/** Remove quotes from an IMAP string */
|
|
238
|
+
function unquote(s) {
|
|
239
|
+
if (!s || s === "NIL")
|
|
240
|
+
return "";
|
|
241
|
+
if (s.startsWith('"') && s.endsWith('"'))
|
|
242
|
+
return s.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
243
|
+
return s;
|
|
244
|
+
}
|
|
245
|
+
/** Decode IMAP encoded-word (=?charset?encoding?text?=) */
|
|
246
|
+
function decodeImapString(s) {
|
|
247
|
+
if (!s)
|
|
248
|
+
return "";
|
|
249
|
+
return s.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_match, charset, encoding, text) => {
|
|
250
|
+
try {
|
|
251
|
+
const cs = charset.toLowerCase().replace(/^utf8$/, "utf-8");
|
|
252
|
+
if (encoding.toUpperCase() === "B") {
|
|
253
|
+
const raw = atob(text);
|
|
254
|
+
const bytes = new Uint8Array(raw.length);
|
|
255
|
+
for (let j = 0; j < raw.length; j++)
|
|
256
|
+
bytes[j] = raw.charCodeAt(j);
|
|
257
|
+
return new TextDecoder(cs).decode(bytes);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Quoted-printable: collect bytes then decode with charset
|
|
261
|
+
const decoded = text.replace(/_/g, " ");
|
|
262
|
+
const bytes = [];
|
|
263
|
+
let i = 0;
|
|
264
|
+
while (i < decoded.length) {
|
|
265
|
+
if (decoded[i] === "=" && i + 2 < decoded.length) {
|
|
266
|
+
bytes.push(parseInt(decoded.substring(i + 1, i + 3), 16));
|
|
267
|
+
i += 3;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
bytes.push(decoded.charCodeAt(i));
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return new TextDecoder(cs).decode(new Uint8Array(bytes));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return text;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/** Tokenize a parenthesized IMAP list (top-level only) */
|
|
283
|
+
function tokenizeParenList(s) {
|
|
284
|
+
const tokens = [];
|
|
285
|
+
let i = 0;
|
|
286
|
+
const str = s.trim();
|
|
287
|
+
// Skip outer parens
|
|
288
|
+
const start = str.startsWith("(") ? 1 : 0;
|
|
289
|
+
const end = str.endsWith(")") ? str.length - 1 : str.length;
|
|
290
|
+
i = start;
|
|
291
|
+
while (i < end) {
|
|
292
|
+
// Skip whitespace
|
|
293
|
+
while (i < end && str[i] === " ")
|
|
294
|
+
i++;
|
|
295
|
+
if (i >= end)
|
|
296
|
+
break;
|
|
297
|
+
if (str[i] === "(") {
|
|
298
|
+
// Nested paren group — find matching close
|
|
299
|
+
let depth = 1;
|
|
300
|
+
let j = i + 1;
|
|
301
|
+
while (j < end && depth > 0) {
|
|
302
|
+
if (str[j] === "(")
|
|
303
|
+
depth++;
|
|
304
|
+
else if (str[j] === ")")
|
|
305
|
+
depth--;
|
|
306
|
+
j++;
|
|
307
|
+
}
|
|
308
|
+
tokens.push(str.substring(i, j));
|
|
309
|
+
i = j;
|
|
310
|
+
}
|
|
311
|
+
else if (str[i] === '"') {
|
|
312
|
+
// Quoted string
|
|
313
|
+
let j = i + 1;
|
|
314
|
+
while (j < end) {
|
|
315
|
+
if (str[j] === "\\" && j + 1 < end) {
|
|
316
|
+
j += 2;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (str[j] === '"') {
|
|
320
|
+
j++;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
j++;
|
|
324
|
+
}
|
|
325
|
+
tokens.push(str.substring(i, j));
|
|
326
|
+
i = j;
|
|
327
|
+
}
|
|
328
|
+
else if (str.substring(i, i + 3).toUpperCase() === "NIL") {
|
|
329
|
+
tokens.push("NIL");
|
|
330
|
+
i += 3;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Atom
|
|
334
|
+
let j = i;
|
|
335
|
+
while (j < end && str[j] !== " " && str[j] !== ")" && str[j] !== "(")
|
|
336
|
+
j++;
|
|
337
|
+
tokens.push(str.substring(i, j));
|
|
338
|
+
i = j;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return tokens;
|
|
342
|
+
}
|
|
343
|
+
/** Parse an IMAP address list: ((name NIL mailbox host) ...) */
|
|
344
|
+
function parseAddressList(token) {
|
|
345
|
+
if (!token || token === "NIL")
|
|
346
|
+
return [];
|
|
347
|
+
const addrs = [];
|
|
348
|
+
// Tokenize the outer list to get each address group as a paren-delimited token
|
|
349
|
+
// e.g. ((name NIL mailbox host)(name2 NIL mailbox2 host2)) → ["(name NIL mailbox host)", "(name2 ...)"]
|
|
350
|
+
const groups = tokenizeParenList(token);
|
|
351
|
+
for (const group of groups) {
|
|
352
|
+
if (!group.startsWith("("))
|
|
353
|
+
continue;
|
|
354
|
+
const parts = tokenizeParenList(group);
|
|
355
|
+
if (parts.length >= 4) {
|
|
356
|
+
const name = decodeImapString(unquote(parts[0]));
|
|
357
|
+
const mailbox = unquote(parts[2]);
|
|
358
|
+
const host = unquote(parts[3]);
|
|
359
|
+
const address = mailbox && host ? `${mailbox}@${host}` : mailbox || "";
|
|
360
|
+
addrs.push({ name, address });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return addrs;
|
|
364
|
+
}
|
|
365
|
+
/** Build SEARCH criteria string from common search parameters */
|
|
366
|
+
export function buildSearchCriteria(criteria) {
|
|
367
|
+
const parts = [];
|
|
368
|
+
if (criteria.all)
|
|
369
|
+
parts.push("ALL");
|
|
370
|
+
if (criteria.since)
|
|
371
|
+
parts.push(`SINCE ${formatImapDate(criteria.since)}`);
|
|
372
|
+
if (criteria.before)
|
|
373
|
+
parts.push(`BEFORE ${formatImapDate(criteria.before)}`);
|
|
374
|
+
if (criteria.from)
|
|
375
|
+
parts.push(`FROM "${criteria.from}"`);
|
|
376
|
+
if (criteria.to)
|
|
377
|
+
parts.push(`TO "${criteria.to}"`);
|
|
378
|
+
if (criteria.subject)
|
|
379
|
+
parts.push(`SUBJECT "${criteria.subject}"`);
|
|
380
|
+
if (criteria.body)
|
|
381
|
+
parts.push(`BODY "${criteria.body}"`);
|
|
382
|
+
if (criteria.uid)
|
|
383
|
+
parts.push(`UID ${criteria.uid}`);
|
|
384
|
+
return parts.length > 0 ? parts.join(" ") : "ALL";
|
|
385
|
+
}
|
|
386
|
+
/** Format a date for IMAP (DD-Mon-YYYY) */
|
|
387
|
+
function formatImapDate(d) {
|
|
388
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
389
|
+
return `${d.getDate()}-${months[d.getMonth()]}-${d.getFullYear()}`;
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=imap-protocol.js.map
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bobfrankston/iflow-direct — direct IMAP client, transport-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* No Node.js dependencies. Works in browser via import maps.
|
|
5
|
+
* Callers provide a TransportFactory:
|
|
6
|
+
* - Desktop: NodeTransport from @bobfrankston/iflow-node
|
|
7
|
+
* - Browser/WebView: BridgeTransport (included here, uses msgapi.tcp)
|
|
8
|
+
*/
|
|
9
|
+
export type { ImapClientConfig, TokenProvider } from "./types.js";
|
|
10
|
+
export type { ImapTransport, TransportFactory } from "./transport.js";
|
|
11
|
+
export { BridgeTransport } from "./bridge-transport.js";
|
|
12
|
+
export * as proto from "./imap-protocol.js";
|
|
13
|
+
export { NativeImapClient } from "./imap-native.js";
|
|
14
|
+
export type { NativeFetchedMessage, NativeFolder, MailboxInfo } from "./imap-native.js";
|
|
15
|
+
export { CompatImapClient } from "./imap-compat.js";
|
|
16
|
+
export type { SpecialFolders } from "./imap-compat.js";
|
|
17
|
+
export { isGmailUser, isGmailServer, createGmailConfig, createAutoImapConfig, type GmailOAuthConfig, } from "./gmail.js";
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bobfrankston/iflow-direct — direct IMAP client, transport-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* No Node.js dependencies. Works in browser via import maps.
|
|
5
|
+
* Callers provide a TransportFactory:
|
|
6
|
+
* - Desktop: NodeTransport from @bobfrankston/iflow-node
|
|
7
|
+
* - Browser/WebView: BridgeTransport (included here, uses msgapi.tcp)
|
|
8
|
+
*/
|
|
9
|
+
export { BridgeTransport } from "./bridge-transport.js";
|
|
10
|
+
// Protocol (pure JS, no I/O)
|
|
11
|
+
export * as proto from "./imap-protocol.js";
|
|
12
|
+
// Native IMAP client (transport-agnostic)
|
|
13
|
+
export { NativeImapClient } from "./imap-native.js";
|
|
14
|
+
// Compatibility wrapper (old ImapClient API shape)
|
|
15
|
+
export { CompatImapClient } from "./imap-compat.js";
|
|
16
|
+
// Gmail OAuth support (Node-free — caller provides tokenProvider)
|
|
17
|
+
export { isGmailUser, isGmailServer, createGmailConfig, createAutoImapConfig, } from "./gmail.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobfrankston/iflow-direct",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"watch": "tsc -watch",
|
|
11
|
+
"check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"imap",
|
|
15
|
+
"email",
|
|
16
|
+
"browser",
|
|
17
|
+
"transport-agnostic"
|
|
18
|
+
],
|
|
19
|
+
"author": "Bob Frankston",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./index.ts",
|
|
24
|
+
"default": "./index.js"
|
|
25
|
+
},
|
|
26
|
+
"./bridge-transport": {
|
|
27
|
+
"types": "./bridge-transport.ts",
|
|
28
|
+
"default": "./bridge-transport.js"
|
|
29
|
+
},
|
|
30
|
+
"./types": {
|
|
31
|
+
"types": "./types.ts",
|
|
32
|
+
"default": "./types.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/BobFrankston/iflow-direct.git"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/transport.d.ts
ADDED
|
@@ -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/transport.js
ADDED
package/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for iflow-common — no imapflow dependency.
|
|
3
|
+
*/
|
|
4
|
+
export type TokenProvider = () => Promise<string>;
|
|
5
|
+
export interface ImapClientConfig {
|
|
6
|
+
server: string;
|
|
7
|
+
port: number;
|
|
8
|
+
username: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
tokenProvider?: TokenProvider;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
rejectUnauthorized?: boolean;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.d.ts.map
|