@emailthing/cli 0.0.0-alpha.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 +24 -0
- package/dist/agent.js +359 -0
- package/dist/config-cfsae2jm.js +154 -0
- package/dist/index-afyvwmt5.js +47 -0
- package/dist/index-xp4bqj3r.js +180 -0
- package/dist/index.js +17 -0
- package/dist/main.js +1082 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<a href="https://emailthing.app/home" target="_blank">
|
|
3
|
+
<img src="https://emailthing.app/logo.png" alt="EmailThing Logo" width="84">
|
|
4
|
+
</a>
|
|
5
|
+
<br>
|
|
6
|
+
EmailThing (CLI)
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
An interactive terminal email client with AI agent mode for email automation.
|
|
10
|
+
|
|
11
|
+
Modes:
|
|
12
|
+
* Normal interactive: navigate mailboxes, read and compose emails from your terminal
|
|
13
|
+
* Agent mode: let your agent interact with your emails via command line
|
|
14
|
+
|
|
15
|
+
> Note: This package requires [Bun](https://bun.com/) as its runtime.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
$ bunx @emailthing/cli
|
|
19
|
+
# Welcome to EmailThing CLI!
|
|
20
|
+
# <interactive terminal UI starts here>
|
|
21
|
+
|
|
22
|
+
$ bunx @emailthing/cli agent list # see your email list
|
|
23
|
+
$ bunx @emailthing/cli agent email id12345 # see that email
|
|
24
|
+
```
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import {
|
|
4
|
+
EmailThingCLI,
|
|
5
|
+
syncData
|
|
6
|
+
} from "./index-xp4bqj3r.js";
|
|
7
|
+
import {
|
|
8
|
+
getDB,
|
|
9
|
+
loadAuth
|
|
10
|
+
} from "./config-cfsae2jm.js";
|
|
11
|
+
import"./index-afyvwmt5.js";
|
|
12
|
+
|
|
13
|
+
// src/agent.ts
|
|
14
|
+
import { parseArgs } from "util";
|
|
15
|
+
async function main() {
|
|
16
|
+
let { values: globalValues, positionals: rootPositionals } = parseArgs({
|
|
17
|
+
options: {
|
|
18
|
+
help: { type: "boolean", short: "h", default: !1 },
|
|
19
|
+
sync: { type: "boolean", default: !1 }
|
|
20
|
+
},
|
|
21
|
+
allowPositionals: !0,
|
|
22
|
+
strict: !1,
|
|
23
|
+
allowNegative: !0
|
|
24
|
+
}), agentIndex = rootPositionals.findIndex((arg) => arg === "--agent" || arg === "agent"), subcmd = rootPositionals[agentIndex + 1], subcmdArgs = rootPositionals.slice(agentIndex + 2);
|
|
25
|
+
if (globalValues.help || !subcmd || !["list", "email", "sync"].includes(subcmd)) {
|
|
26
|
+
let nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
27
|
+
console.log(`
|
|
28
|
+
EmailThing CLI - Agent Mode
|
|
29
|
+
|
|
30
|
+
AI-friendly command-line interface for email automation.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
bunx @emailthing/cli agent <subcommand> [options]
|
|
34
|
+
|
|
35
|
+
Subcommands:
|
|
36
|
+
list List recent emails (default: 25)
|
|
37
|
+
email <id> Show full email by ID (body, etc)
|
|
38
|
+
sync Force fresh sync from server
|
|
39
|
+
|
|
40
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
41
|
+
|
|
42
|
+
agent list [filters]:
|
|
43
|
+
--unread Show only unread emails
|
|
44
|
+
--read Show only read emails
|
|
45
|
+
--starred Show only starred emails
|
|
46
|
+
--from=<email> Filter by sender
|
|
47
|
+
--subject=<text> Search subject only
|
|
48
|
+
--search=<query> Search subject, from, and body
|
|
49
|
+
--categoryid=<id> Filter by category
|
|
50
|
+
--before=<date> Emails before ISO date
|
|
51
|
+
--after=<date> Emails after ISO date
|
|
52
|
+
--limit=<n> Number of emails to return (default: 25)
|
|
53
|
+
--offset=<n> Skip first N emails (pagination)
|
|
54
|
+
--json Output as JSON (default: plain text)
|
|
55
|
+
--count Only show count of matching emails
|
|
56
|
+
--fields=a,b,c Show only specific fields (id,from,subject,date,snippet)
|
|
57
|
+
--sync Force a sync of emails before listing (recommended if new mail expected)
|
|
58
|
+
|
|
59
|
+
agent email <id>:
|
|
60
|
+
--json Output complete email as JSON
|
|
61
|
+
--fields=a,b,c Show only those fields
|
|
62
|
+
--sync Force sync before showing email details
|
|
63
|
+
|
|
64
|
+
agent sync:
|
|
65
|
+
Triggers sync with server to update local cache
|
|
66
|
+
(No other options needed)
|
|
67
|
+
|
|
68
|
+
Global Options:
|
|
69
|
+
-h, --help Show this help message
|
|
70
|
+
--sync Force fresh sync before any subcommand
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
bunx @emailthing/cli agent list --unread --limit=10 --sync
|
|
74
|
+
bunx @emailthing/cli agent email abc123xyz789example456id --json --sync
|
|
75
|
+
bunx @emailthing/cli agent sync
|
|
76
|
+
|
|
77
|
+
Output Format:
|
|
78
|
+
List mode:
|
|
79
|
+
Email 1:
|
|
80
|
+
ID: abc123 # Use this ID with 'agent email <id>'
|
|
81
|
+
From: sender@example.com
|
|
82
|
+
Subject: Your 2FA code
|
|
83
|
+
Date: ${nowIso}
|
|
84
|
+
Read: No
|
|
85
|
+
Category: Security (cat123)
|
|
86
|
+
Snippet: Your code is 123456...
|
|
87
|
+
Detail mode:
|
|
88
|
+
Email Details:
|
|
89
|
+
ID: abc123
|
|
90
|
+
From: sender@example.com
|
|
91
|
+
To: you@example.com
|
|
92
|
+
CC: (none)
|
|
93
|
+
Subject: Your 2FA code
|
|
94
|
+
Date: ${nowIso}
|
|
95
|
+
Read: Yes
|
|
96
|
+
Starred: No
|
|
97
|
+
Body:
|
|
98
|
+
Your verification code is: 123456
|
|
99
|
+
This code expires in 10 minutes.
|
|
100
|
+
`), process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
let db = getDB(), client = new EmailThingCLI, auth = loadAuth(db);
|
|
103
|
+
if (!auth)
|
|
104
|
+
console.error("Error: Not authenticated. Please run the CLI first to login."), process.exit(1);
|
|
105
|
+
client.setAuth(auth.token, auth.refreshToken, auth.tokenExpiresAt);
|
|
106
|
+
let syncState = db.query("SELECT lastSync FROM sync_state WHERE id = 1").get(), lastSyncTime = syncState?.lastSync ? new Date(syncState.lastSync).getTime() : 0, TEN_MINUTES_MS = 600000, tenMinutesAgo = Date.now() - TEN_MINUTES_MS;
|
|
107
|
+
if (globalValues.sync || lastSyncTime < tenMinutesAgo)
|
|
108
|
+
try {
|
|
109
|
+
await syncData(client, db, !1);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("Sync failed:", error instanceof Error ? error.message : error), console.error("Showing stale cached data...");
|
|
112
|
+
}
|
|
113
|
+
if (subcmd === "sync") {
|
|
114
|
+
let db2 = getDB(), client2 = new EmailThingCLI, auth2 = loadAuth(db2);
|
|
115
|
+
if (!auth2)
|
|
116
|
+
console.error("Error: Not authenticated. Please run the CLI first to login."), process.exit(1);
|
|
117
|
+
client2.setAuth(auth2.token, auth2.refreshToken, auth2.tokenExpiresAt), await syncData(client2, db2, !1), console.log("Sync complete. Local cache updated."), db2.close(), process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
if (subcmd === "email") {
|
|
120
|
+
let parseResult = parseArgs({
|
|
121
|
+
options: {
|
|
122
|
+
json: { type: "boolean", default: !1 },
|
|
123
|
+
fields: { type: "string" },
|
|
124
|
+
sync: { type: "boolean", default: !1 }
|
|
125
|
+
},
|
|
126
|
+
allowPositionals: !0,
|
|
127
|
+
strict: !0,
|
|
128
|
+
allowNegative: !0
|
|
129
|
+
}), values = { ...parseResult.values }, positionals = parseResult.positionals, emailId = positionals[positionals.findIndex((arg) => arg === "email") + 1], syncFlag = globalValues.sync || values.sync, db2 = getDB(), client2 = new EmailThingCLI, auth2 = loadAuth(db2);
|
|
130
|
+
if (!auth2)
|
|
131
|
+
console.error("Error: Not authenticated. Please run the CLI first to login."), process.exit(1);
|
|
132
|
+
if (client2.setAuth(auth2.token, auth2.refreshToken, auth2.tokenExpiresAt), syncFlag)
|
|
133
|
+
await syncData(client2, db2, !1);
|
|
134
|
+
if (!emailId)
|
|
135
|
+
console.error(`Error: You must provide an email ID.
|
|
136
|
+
Usage: bunx @emailthing/cli agent email <id>`), process.exit(1);
|
|
137
|
+
let jsonOutput = values.json, fields = values.fields ? values.fields.split(",") : void 0, email = db2.query(`
|
|
138
|
+
SELECT
|
|
139
|
+
e.*,
|
|
140
|
+
c.name as categoryName
|
|
141
|
+
FROM emails e
|
|
142
|
+
LEFT JOIN categories c
|
|
143
|
+
ON e.categoryId = c.id AND c.isDeleted = 0
|
|
144
|
+
WHERE e.id = ?
|
|
145
|
+
`).get(emailId);
|
|
146
|
+
if (!email)
|
|
147
|
+
console.error(`Error: Email ${emailId} not found`), process.exit(1);
|
|
148
|
+
if (jsonOutput) {
|
|
149
|
+
let emailData = {
|
|
150
|
+
id: email.id,
|
|
151
|
+
mailboxId: email.mailboxId,
|
|
152
|
+
mailboxAddress: email.mailboxAddress,
|
|
153
|
+
from: email.from_addr,
|
|
154
|
+
to: email.to_addr,
|
|
155
|
+
cc: email.cc,
|
|
156
|
+
bcc: email.bcc,
|
|
157
|
+
replyTo: email.replyTo,
|
|
158
|
+
subject: email.subject,
|
|
159
|
+
body: email.body,
|
|
160
|
+
html: email.html,
|
|
161
|
+
snippet: email.snippet,
|
|
162
|
+
isRead: email.isRead === 1,
|
|
163
|
+
isStarred: email.isStarred === 1,
|
|
164
|
+
createdAt: email.createdAt,
|
|
165
|
+
categoryId: email.categoryId,
|
|
166
|
+
categoryName: email.categoryName,
|
|
167
|
+
headers: email.headers ? JSON.parse(email.headers) : null
|
|
168
|
+
}, output = emailData;
|
|
169
|
+
if (fields)
|
|
170
|
+
output = fields.reduce((acc, f) => {
|
|
171
|
+
if (f in emailData)
|
|
172
|
+
acc[f] = emailData[f];
|
|
173
|
+
return acc;
|
|
174
|
+
}, {});
|
|
175
|
+
console.log(JSON.stringify(output, null, 2));
|
|
176
|
+
} else {
|
|
177
|
+
let lines = [
|
|
178
|
+
"Email Details:",
|
|
179
|
+
` ID: ${email.id}`,
|
|
180
|
+
` From: ${email.from_addr}`,
|
|
181
|
+
` To: ${email.to_addr}`,
|
|
182
|
+
` CC: ${email.cc || "(none)"}`,
|
|
183
|
+
` Subject: ${email.subject || "(no subject)"}`,
|
|
184
|
+
` Date: ${email.createdAt}`,
|
|
185
|
+
` Read: ${email.isRead === 1 ? "Yes" : "No"}`,
|
|
186
|
+
` Starred: ${email.isStarred === 1 ? "Yes" : "No"}`,
|
|
187
|
+
email.categoryName ? ` Category: ${email.categoryName} (${email.categoryId})` : "",
|
|
188
|
+
" Body:"
|
|
189
|
+
], body = (email.body || email.html || "(empty)").split(`
|
|
190
|
+
`).map((line) => " " + line).join(`
|
|
191
|
+
`);
|
|
192
|
+
if (lines.push(body), fields)
|
|
193
|
+
lines = ["Email Details:"], fields.forEach((f) => {
|
|
194
|
+
if (f === "body")
|
|
195
|
+
lines.push(`Body:
|
|
196
|
+
${body}`);
|
|
197
|
+
else if (f in email)
|
|
198
|
+
lines.push(` ${f}: ${email[f]}`);
|
|
199
|
+
});
|
|
200
|
+
console.log(lines.join(`
|
|
201
|
+
`));
|
|
202
|
+
}
|
|
203
|
+
db2.close();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (subcmd === "list") {
|
|
207
|
+
let values = { ...parseArgs({
|
|
208
|
+
options: {
|
|
209
|
+
unread: { type: "boolean" },
|
|
210
|
+
read: { type: "boolean" },
|
|
211
|
+
starred: { type: "boolean" },
|
|
212
|
+
from: { type: "string" },
|
|
213
|
+
subject: { type: "string" },
|
|
214
|
+
search: { type: "string" },
|
|
215
|
+
categoryid: { type: "string" },
|
|
216
|
+
before: { type: "string" },
|
|
217
|
+
after: { type: "string" },
|
|
218
|
+
limit: { type: "string", default: "25" },
|
|
219
|
+
offset: { type: "string", default: "0" },
|
|
220
|
+
json: { type: "boolean", default: !1 },
|
|
221
|
+
count: { type: "boolean", default: !1 },
|
|
222
|
+
fields: { type: "string" },
|
|
223
|
+
sync: { type: "boolean", default: !1 }
|
|
224
|
+
},
|
|
225
|
+
allowPositionals: !0,
|
|
226
|
+
strict: !0,
|
|
227
|
+
allowNegative: !0
|
|
228
|
+
}).values }, db2 = getDB(), client2 = new EmailThingCLI, auth2 = loadAuth(db2);
|
|
229
|
+
if (!auth2)
|
|
230
|
+
console.error("Error: Not authenticated. Please run the CLI first to login."), process.exit(1);
|
|
231
|
+
client2.setAuth(auth2.token, auth2.refreshToken, auth2.tokenExpiresAt);
|
|
232
|
+
let limit = values.limit ? parseInt(values.limit) : 25, offset = values.offset ? parseInt(values.offset) : 0, fields = values.fields ? values.fields.split(",") : void 0, query = `
|
|
233
|
+
SELECT
|
|
234
|
+
e.id,
|
|
235
|
+
e.subject,
|
|
236
|
+
e.from_addr,
|
|
237
|
+
e.snippet,
|
|
238
|
+
e.isRead,
|
|
239
|
+
e.isStarred,
|
|
240
|
+
e.createdAt,
|
|
241
|
+
e.categoryId,
|
|
242
|
+
c.name as categoryName
|
|
243
|
+
FROM emails e
|
|
244
|
+
LEFT JOIN categories c
|
|
245
|
+
ON e.categoryId = c.id AND c.isDeleted = 0
|
|
246
|
+
WHERE 1=1
|
|
247
|
+
`, extraFilters = { query: "", params: [] }, params = [];
|
|
248
|
+
if (values.before)
|
|
249
|
+
extraFilters.query += " AND e.createdAt < ?", extraFilters.params.push(values.before);
|
|
250
|
+
if (values.after)
|
|
251
|
+
extraFilters.query += " AND e.createdAt > ?", extraFilters.params.push(values.after);
|
|
252
|
+
if (values.search) {
|
|
253
|
+
extraFilters.query += " AND (e.subject LIKE ? OR e.from_addr LIKE ? OR e.body LIKE ?)";
|
|
254
|
+
let searchPattern = `%${values.search}%`;
|
|
255
|
+
extraFilters.params.push(searchPattern, searchPattern, searchPattern);
|
|
256
|
+
}
|
|
257
|
+
if (values.categoryid)
|
|
258
|
+
extraFilters.query += " AND e.categoryId = ?", extraFilters.params.push(values.categoryid);
|
|
259
|
+
if (values.unread !== void 0)
|
|
260
|
+
extraFilters.query += " AND e.isRead = 0";
|
|
261
|
+
if (values.read !== void 0)
|
|
262
|
+
extraFilters.query += " AND e.isRead = 1";
|
|
263
|
+
if (values.starred !== void 0)
|
|
264
|
+
extraFilters.query += " AND e.isStarred = 1";
|
|
265
|
+
if (values.from)
|
|
266
|
+
extraFilters.query += " AND e.from_addr LIKE ?", extraFilters.params.push(`%${values.from}%`);
|
|
267
|
+
if (values.subject)
|
|
268
|
+
extraFilters.query += " AND e.subject LIKE ?", extraFilters.params.push(`%${values.subject}%`);
|
|
269
|
+
query += extraFilters.query, params.push(...extraFilters.params), query += " ORDER BY e.createdAt DESC LIMIT ? OFFSET ?", params.push(limit, offset);
|
|
270
|
+
let emails = db2.query(query).all(...params), countQuery = "SELECT COUNT(*) as total FROM emails e WHERE 1=1";
|
|
271
|
+
countQuery += extraFilters.query;
|
|
272
|
+
let totalResult = db2.query(countQuery).get(...extraFilters.params), totalPages = Math.ceil(totalResult.total / limit), currentPage = Math.floor(offset / limit) + 1, hasMore = offset + emails.length < totalResult.total;
|
|
273
|
+
if (values.count) {
|
|
274
|
+
if (values.json)
|
|
275
|
+
console.log(JSON.stringify({ count: totalResult.total }));
|
|
276
|
+
else
|
|
277
|
+
console.log(totalResult.total);
|
|
278
|
+
db2.close();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (values.json) {
|
|
282
|
+
let emailsData = emails.map((e) => {
|
|
283
|
+
let baseEmail = {
|
|
284
|
+
id: e.id,
|
|
285
|
+
from: e.from_addr,
|
|
286
|
+
subject: e.subject,
|
|
287
|
+
date: e.createdAt,
|
|
288
|
+
isRead: e.isRead === 1,
|
|
289
|
+
isStarred: e.isStarred === 1,
|
|
290
|
+
snippet: e.snippet
|
|
291
|
+
};
|
|
292
|
+
if (e.categoryName)
|
|
293
|
+
baseEmail.category = { id: e.categoryId, name: e.categoryName };
|
|
294
|
+
if (fields) {
|
|
295
|
+
let filtered = {};
|
|
296
|
+
return fields.forEach((field) => {
|
|
297
|
+
if (field in baseEmail)
|
|
298
|
+
filtered[field] = baseEmail[field];
|
|
299
|
+
}), filtered;
|
|
300
|
+
}
|
|
301
|
+
return baseEmail;
|
|
302
|
+
});
|
|
303
|
+
console.log(JSON.stringify({
|
|
304
|
+
count: emails.length,
|
|
305
|
+
total: totalResult.total,
|
|
306
|
+
limit,
|
|
307
|
+
offset,
|
|
308
|
+
hasMore,
|
|
309
|
+
emails: emailsData
|
|
310
|
+
}, null, 2)), db2.close();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let aliases = db2.query("SELECT * FROM mailbox_aliases WHERE `default` = 1 AND isDeleted = 0").all();
|
|
314
|
+
console.log(`Email List:
|
|
315
|
+
|
|
316
|
+
For mailboxes: ${aliases.map((a) => a.alias).join(", ")} (default aliases)
|
|
317
|
+
|
|
318
|
+
Showing ${emails.length} of ${totalResult.total} emails (page ${currentPage}/${totalPages})
|
|
319
|
+
${hasMore ? `Next page: --offset=${offset + limit} --limit=${limit}` : "No more emails"}
|
|
320
|
+
`), emails.forEach((email, idx) => {
|
|
321
|
+
let absoluteNumber = offset + idx + 1;
|
|
322
|
+
if (fields) {
|
|
323
|
+
let output = [`Email ${absoluteNumber}:`];
|
|
324
|
+
if (fields.includes("id"))
|
|
325
|
+
output.push(` ID: ${email.id}`);
|
|
326
|
+
if (fields.includes("from"))
|
|
327
|
+
output.push(` From: ${email.from_addr}`);
|
|
328
|
+
if (fields.includes("subject"))
|
|
329
|
+
output.push(` Subject: ${email.subject || "(no subject)"}`);
|
|
330
|
+
if (fields.includes("date"))
|
|
331
|
+
output.push(` Date: ${email.createdAt}`);
|
|
332
|
+
if (fields.includes("snippet"))
|
|
333
|
+
output.push(` Snippet: ${email.snippet?.replaceAll(`
|
|
334
|
+
`, " ")?.trim() || "(no preview)"}`);
|
|
335
|
+
console.log(output.join(`
|
|
336
|
+
`) + `
|
|
337
|
+
`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let categoryInfo = email.categoryName ? `
|
|
341
|
+
Category: ${email.categoryName} (${email.categoryId})` : "";
|
|
342
|
+
console.log(`Email ${absoluteNumber}:
|
|
343
|
+
ID: ${email.id}
|
|
344
|
+
From: ${email.from_addr}
|
|
345
|
+
Subject: ${email.subject || "(no subject)"}
|
|
346
|
+
Date: ${email.createdAt}
|
|
347
|
+
Read: ${email.isRead === 1 ? "Yes" : "No"}
|
|
348
|
+
Starred: ${email.isStarred === 1 ? "Yes" : "No"}${categoryInfo}
|
|
349
|
+
Snippet: ${email.snippet?.replaceAll(`
|
|
350
|
+
`, " ")?.trim() || "(no preview)"}
|
|
351
|
+
|
|
352
|
+
`);
|
|
353
|
+
}), db2.close();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
main().catch((error) => {
|
|
358
|
+
console.error("Error:", error.message, error instanceof Error ? error.stack : error), process.exit(1);
|
|
359
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import"./index-afyvwmt5.js";
|
|
3
|
+
|
|
4
|
+
// src/utils/config.ts
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { existsSync, mkdirSync } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/db/schema.ts
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
function initDB(dbPath) {
|
|
12
|
+
let db = new Database(dbPath);
|
|
13
|
+
db.run("PRAGMA journal_mode = WAL;");
|
|
14
|
+
let currentVersion = db.query("PRAGMA user_version").get().user_version;
|
|
15
|
+
if (db.run(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS auth (
|
|
17
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
18
|
+
token TEXT NOT NULL,
|
|
19
|
+
refreshToken TEXT NOT NULL,
|
|
20
|
+
tokenExpiresAt TEXT NOT NULL,
|
|
21
|
+
refreshTokenExpiresAt TEXT NOT NULL,
|
|
22
|
+
userId TEXT NOT NULL,
|
|
23
|
+
mailboxes TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS emails (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
mailboxId TEXT NOT NULL,
|
|
29
|
+
subject TEXT,
|
|
30
|
+
from_addr TEXT,
|
|
31
|
+
to_addr TEXT,
|
|
32
|
+
cc TEXT,
|
|
33
|
+
bcc TEXT,
|
|
34
|
+
replyTo TEXT,
|
|
35
|
+
body TEXT,
|
|
36
|
+
html TEXT,
|
|
37
|
+
snippet TEXT,
|
|
38
|
+
isRead BOOLEAN DEFAULT FALSE,
|
|
39
|
+
isStarred BOOLEAN DEFAULT FALSE,
|
|
40
|
+
createdAt TEXT NOT NULL,
|
|
41
|
+
headers TEXT,
|
|
42
|
+
raw TEXT,
|
|
43
|
+
categoryId TEXT,
|
|
44
|
+
isDeleted BOOLEAN DEFAULT FALSE
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS mailboxes (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
isDeleted BOOLEAN DEFAULT FALSE
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
mailboxId TEXT NOT NULL,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
color TEXT,
|
|
57
|
+
order_num INTEGER DEFAULT 0,
|
|
58
|
+
isDeleted BOOLEAN DEFAULT FALSE
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS drafts (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
mailboxId TEXT NOT NULL,
|
|
64
|
+
subject TEXT,
|
|
65
|
+
body TEXT,
|
|
66
|
+
html TEXT,
|
|
67
|
+
to_addr TEXT,
|
|
68
|
+
cc TEXT,
|
|
69
|
+
bcc TEXT,
|
|
70
|
+
from_addr TEXT,
|
|
71
|
+
updatedAt TEXT NOT NULL,
|
|
72
|
+
isDeleted BOOLEAN DEFAULT FALSE
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS mailbox_aliases (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
mailboxId TEXT NOT NULL,
|
|
78
|
+
alias TEXT NOT NULL,
|
|
79
|
+
name TEXT,
|
|
80
|
+
createdAt TEXT NOT NULL,
|
|
81
|
+
updatedAt TEXT NOT NULL,
|
|
82
|
+
"default" BOOLEAN DEFAULT FALSE,
|
|
83
|
+
isDeleted BOOLEAN DEFAULT FALSE
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_aliases_mailboxId ON mailbox_aliases(mailboxId);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
88
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
89
|
+
lastSync TEXT
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_emails_mailbox ON emails(mailboxId);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_emails_created ON emails(createdAt);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_categories_mailbox ON categories(mailboxId);
|
|
95
|
+
`), currentVersion < 1) {
|
|
96
|
+
try {
|
|
97
|
+
db.run("ALTER TABLE emails ADD COLUMN categoryId TEXT");
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
db.run("PRAGMA user_version = 1");
|
|
100
|
+
}
|
|
101
|
+
return db;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/utils/config.ts
|
|
105
|
+
var CONFIG_DIR = join(homedir(), ".emailthing"), DB_PATH = join(CONFIG_DIR, "data.db");
|
|
106
|
+
function ensureConfigDir() {
|
|
107
|
+
if (!existsSync(CONFIG_DIR))
|
|
108
|
+
mkdirSync(CONFIG_DIR, { recursive: !0 });
|
|
109
|
+
}
|
|
110
|
+
function getDB() {
|
|
111
|
+
return ensureConfigDir(), initDB(DB_PATH);
|
|
112
|
+
}
|
|
113
|
+
function loadAuth(db) {
|
|
114
|
+
let auth = db.query("SELECT * FROM auth WHERE id = 1").get();
|
|
115
|
+
return auth ? {
|
|
116
|
+
token: auth.token,
|
|
117
|
+
refreshToken: auth.refreshToken,
|
|
118
|
+
tokenExpiresAt: auth.tokenExpiresAt,
|
|
119
|
+
refreshTokenExpiresAt: auth.refreshTokenExpiresAt,
|
|
120
|
+
userId: auth.userId,
|
|
121
|
+
mailboxes: JSON.parse(auth.mailboxes)
|
|
122
|
+
} : null;
|
|
123
|
+
}
|
|
124
|
+
function saveAuth(db, auth) {
|
|
125
|
+
db.run(`
|
|
126
|
+
INSERT OR REPLACE INTO auth (id, token, refreshToken, tokenExpiresAt, refreshTokenExpiresAt, userId, mailboxes)
|
|
127
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
128
|
+
`, [
|
|
129
|
+
auth.token,
|
|
130
|
+
auth.refreshToken,
|
|
131
|
+
auth.tokenExpiresAt,
|
|
132
|
+
auth.refreshTokenExpiresAt,
|
|
133
|
+
auth.userId,
|
|
134
|
+
JSON.stringify(auth.mailboxes)
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
function clearAuth(db) {
|
|
138
|
+
db.run("DELETE FROM auth WHERE id = 1");
|
|
139
|
+
}
|
|
140
|
+
function resetDB(db) {
|
|
141
|
+
db.run("DELETE FROM auth"), db.run("DELETE FROM emails"), db.run("DELETE FROM mailboxes"), db.run("DELETE FROM categories"), db.run("DELETE FROM drafts"), db.run("DELETE FROM mailbox_aliases"), db.run("DELETE FROM sync_state");
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
saveAuth,
|
|
145
|
+
resetDB,
|
|
146
|
+
loadAuth,
|
|
147
|
+
getDB,
|
|
148
|
+
ensureConfigDir,
|
|
149
|
+
clearAuth,
|
|
150
|
+
DB_PATH,
|
|
151
|
+
CONFIG_DIR
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export { getDB, loadAuth, saveAuth };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var { getPrototypeOf: __getProtoOf, defineProperty: __defProp, getOwnPropertyNames: __getOwnPropNames } = Object;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
6
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
7
|
+
let to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target;
|
|
8
|
+
for (let key of __getOwnPropNames(mod))
|
|
9
|
+
if (!__hasOwnProp.call(to, key))
|
|
10
|
+
__defProp(to, key, {
|
|
11
|
+
get: () => mod[key],
|
|
12
|
+
enumerable: !0
|
|
13
|
+
});
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __require = import.meta.require, __using = (stack, value, async) => {
|
|
17
|
+
if (value != null) {
|
|
18
|
+
if (typeof value !== "object" && typeof value !== "function")
|
|
19
|
+
throw TypeError('Object expected to be assigned to "using" declaration');
|
|
20
|
+
let dispose;
|
|
21
|
+
if (async)
|
|
22
|
+
dispose = value[Symbol.asyncDispose];
|
|
23
|
+
if (dispose === void 0)
|
|
24
|
+
dispose = value[Symbol.dispose];
|
|
25
|
+
if (typeof dispose !== "function")
|
|
26
|
+
throw TypeError("Object not disposable");
|
|
27
|
+
stack.push([async, dispose, value]);
|
|
28
|
+
} else if (async)
|
|
29
|
+
stack.push([async]);
|
|
30
|
+
return value;
|
|
31
|
+
}, __callDispose = (stack, error, hasError) => {
|
|
32
|
+
let fail = (e) => error = hasError ? new SuppressedError(e, error, "An error was suppressed during disposal") : (hasError = !0, e), next = (it) => {
|
|
33
|
+
while (it = stack.pop())
|
|
34
|
+
try {
|
|
35
|
+
var result = it[1] && it[1].call(it[2]);
|
|
36
|
+
if (it[0])
|
|
37
|
+
return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fail(e);
|
|
40
|
+
}
|
|
41
|
+
if (hasError)
|
|
42
|
+
throw error;
|
|
43
|
+
};
|
|
44
|
+
return next();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { __toESM, __require, __using, __callDispose };
|