zele 0.3.16 → 0.3.17

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.
Files changed (63) hide show
  1. package/README.md +91 -36
  2. package/dist/api-utils.d.ts +4 -0
  3. package/dist/api-utils.js +6 -0
  4. package/dist/api-utils.js.map +1 -1
  5. package/dist/auth.d.ts +71 -9
  6. package/dist/auth.js +186 -10
  7. package/dist/auth.js.map +1 -1
  8. package/dist/commands/attachment.js +2 -0
  9. package/dist/commands/attachment.js.map +1 -1
  10. package/dist/commands/auth-cmd.js +104 -6
  11. package/dist/commands/auth-cmd.js.map +1 -1
  12. package/dist/commands/draft.js +7 -1
  13. package/dist/commands/draft.js.map +1 -1
  14. package/dist/commands/filter.js +7 -2
  15. package/dist/commands/filter.js.map +1 -1
  16. package/dist/commands/label.js +19 -9
  17. package/dist/commands/label.js.map +1 -1
  18. package/dist/commands/mail-actions.js.map +1 -1
  19. package/dist/commands/mail.js +49 -22
  20. package/dist/commands/mail.js.map +1 -1
  21. package/dist/commands/profile.js +25 -18
  22. package/dist/commands/profile.js.map +1 -1
  23. package/dist/db.js +24 -0
  24. package/dist/db.js.map +1 -1
  25. package/dist/generated/internal/class.js +2 -2
  26. package/dist/generated/internal/class.js.map +1 -1
  27. package/dist/generated/internal/prismaNamespace.d.ts +2 -0
  28. package/dist/generated/internal/prismaNamespace.js +2 -0
  29. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  30. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
  31. package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  33. package/dist/generated/models/Account.d.ts +97 -1
  34. package/dist/gmail-client.d.ts +14 -0
  35. package/dist/gmail-client.js +46 -0
  36. package/dist/gmail-client.js.map +1 -1
  37. package/dist/imap-smtp-client.d.ts +235 -0
  38. package/dist/imap-smtp-client.js +1225 -0
  39. package/dist/imap-smtp-client.js.map +1 -0
  40. package/dist/mail-tui.js.map +1 -1
  41. package/package.json +5 -2
  42. package/schema.prisma +7 -5
  43. package/skills/zele/SKILL.md +50 -21
  44. package/src/api-utils.ts +6 -0
  45. package/src/auth.ts +282 -14
  46. package/src/commands/attachment.ts +1 -0
  47. package/src/commands/auth-cmd.ts +112 -6
  48. package/src/commands/draft.ts +5 -1
  49. package/src/commands/filter.ts +9 -3
  50. package/src/commands/label.ts +22 -11
  51. package/src/commands/mail-actions.ts +2 -1
  52. package/src/commands/mail.ts +52 -22
  53. package/src/commands/profile.ts +27 -17
  54. package/src/db.ts +28 -0
  55. package/src/generated/internal/class.ts +2 -2
  56. package/src/generated/internal/prismaNamespace.ts +2 -0
  57. package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
  58. package/src/generated/models/Account.ts +97 -1
  59. package/src/gmail-client.test.ts +155 -2
  60. package/src/gmail-client.ts +65 -0
  61. package/src/imap-smtp-client.ts +1381 -0
  62. package/src/mail-tui.tsx +2 -1
  63. package/src/schema.sql +2 -0
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
  <br/>
3
3
  <br/>
4
4
  <h3>zele</h3>
5
- <p>Manage emails & calendar from your terminal. For you and your agents</p>
5
+ <p>Email & Calendar CLI Gmail, IMAP/SMTP, Google Calendar. For you and your agents</p>
6
6
  <br/>
7
7
  <br/>
8
8
  </div>
9
9
 
10
10
  ## Install
11
11
 
12
- Multi-account Gmail and Google Calendar client with OAuth2 auth, SQLite cache, and YAML output.
12
+ Multi-account email and calendar client supporting **Google OAuth** and **IMAP/SMTP** (Fastmail, Outlook, any provider). SQLite cache, YAML output.
13
13
 
14
14
  Requires [bun](https://bun.sh):
15
15
 
@@ -24,14 +24,53 @@ bun install -g zele
24
24
 
25
25
  ## Setup
26
26
 
27
+ ### Google accounts
28
+
27
29
  ```bash
28
30
  zele login
29
31
  ```
30
32
 
31
33
  Opens a browser for Google OAuth2. Repeat to add more accounts.
32
34
 
35
+ ### IMAP/SMTP accounts
36
+
37
+ For non-Google providers (Fastmail, Outlook, Gmail with app passwords, any IMAP server):
38
+
39
+ ```bash
40
+ # Fastmail
41
+ zele login imap \
42
+ --email you@fastmail.com \
43
+ --imap-host imap.fastmail.com --imap-port 993 \
44
+ --smtp-host smtp.fastmail.com --smtp-port 465 \
45
+ --password "your-app-password"
46
+
47
+ # Gmail (app password)
48
+ zele login imap \
49
+ --email you@gmail.com \
50
+ --imap-host imap.gmail.com --imap-port 993 \
51
+ --smtp-host smtp.gmail.com --smtp-port 465 \
52
+ --password "your-app-password"
53
+
54
+ # Outlook
55
+ zele login imap \
56
+ --email you@outlook.com \
57
+ --imap-host outlook.office365.com --imap-port 993 \
58
+ --smtp-host smtp-mail.outlook.com --smtp-port 587 \
59
+ --password "your-password"
60
+
61
+ # IMAP-only (no sending)
62
+ zele login imap \
63
+ --email reader@example.com \
64
+ --imap-host imap.example.com --imap-port 993 \
65
+ --password "pass"
66
+ ```
67
+
68
+ Use `--imap-user` / `--smtp-user` if the login username differs from your email. Omit `--smtp-host` for read-only access.
69
+
70
+ ### Account management
71
+
33
72
  ```bash
34
- zele whoami # show authenticated accounts
73
+ zele whoami # show authenticated accounts (type, capabilities)
35
74
  zele logout # remove credentials
36
75
  ```
37
76
 
@@ -68,44 +107,36 @@ zele mail trash-spam
68
107
 
69
108
  ### Search query syntax
70
109
 
71
- `mail search` and `mail watch --query` use [Gmail search operators](https://support.google.com/mail/answer/7190). `mail search` sends the query server-side (full Gmail support), while `mail watch --query` evaluates a subset client-side.
72
-
73
- | Operator | Example | Description |
74
- |---|---|---|
75
- | `from:` | `from:github` | Messages from a sender |
76
- | `to:` | `to:me@example.com` | Messages sent to a recipient |
77
- | `cc:` | `cc:team@example.com` | Messages where recipient was CC'd |
78
- | `subject:` | `subject:invoice` | Messages with words in the subject |
79
- | `is:unread` | `is:unread` | Unread messages |
80
- | `is:read` | `is:read` | Read messages |
81
- | `is:starred` | `is:starred` | Starred messages |
82
- | `has:attachment` | `has:attachment` | Messages with attachments (heuristic in watch) |
83
- | `-` (negate) | `-from:noreply` | Exclude matching messages |
84
- | `" "` (quotes) | `"exact phrase"` | Match an exact phrase |
85
- | `label:` | `label:work` | Messages with a specific label (search only) |
86
- | `in:` | `in:sent` | Messages in a folder (search only) |
87
- | `after:` | `after:2024/01/01` | Messages after a date (search only) |
88
- | `before:` | `before:2024/12/31` | Messages before a date (search only) |
89
- | `newer_than:` | `newer_than:7d` | Messages newer than a period (search only) |
90
- | `older_than:` | `older_than:1m` | Messages older than a period (search only) |
91
- | `filename:` | `filename:pdf` | Attachment filename (search only) |
92
- | `size:` / `larger:` / `smaller:` | `larger:5M` | Filter by message size (search only) |
93
- | `OR` | `from:a OR from:b` | Match either term (search only) |
94
- | `{ }` | `{from:a from:b}` | Group OR terms (search only) |
95
-
96
- Combine multiple operators to narrow results:
110
+ For **Google accounts**, `mail search` and `mail list --filter` use [Gmail search operators](https://support.google.com/mail/answer/7190) server-side. For **IMAP accounts**, queries are translated to IMAP SEARCH criteria (a subset is supported).
111
+
112
+ | Operator | Example | Google | IMAP |
113
+ |---|---|---|---|
114
+ | `from:` | `from:github` | yes | yes |
115
+ | `to:` | `to:me@example.com` | yes | yes |
116
+ | `subject:` | `subject:invoice` | yes | yes |
117
+ | `is:unread` | `is:unread` | yes | yes |
118
+ | `is:starred` | `is:starred` | yes | yes |
119
+ | `has:attachment` | `has:attachment` | yes | yes |
120
+ | `newer_than:` | `newer_than:7d` | yes | yes |
121
+ | `older_than:` | `older_than:1m` | yes | yes |
122
+ | `after:` | `after:2024/01/01` | yes | yes |
123
+ | `before:` | `before:2024/12/31` | yes | yes |
124
+ | `cc:` | `cc:team@example.com` | yes | no |
125
+ | `-` (negate) | `-from:noreply` | yes | no |
126
+ | `" "` (quotes) | `"exact phrase"` | yes | no |
127
+ | `label:` | `label:work` | yes | no |
128
+ | `in:` | `in:sent` | yes | no |
129
+ | `filename:` | `filename:pdf` | yes | no |
130
+ | `size:` / `larger:` / `smaller:` | `larger:5M` | yes | no |
131
+ | `OR` / `{ }` | `from:a OR from:b` | yes | no |
97
132
 
98
133
  ```bash
99
134
  zele mail list --filter "is:unread"
100
- zele mail list --filter "from:github has:attachment" --folder sent
135
+ zele mail list --filter "from:github newer_than:7d" --folder sent
101
136
  zele mail search "from:github is:unread newer_than:7d"
102
137
  zele mail watch --query "from:github has:attachment"
103
138
  ```
104
139
 
105
- `mail list --filter` accepts the same Gmail search operators as `mail search`, and combines with `--folder` and `--label`.
106
-
107
- Operators marked **(search only)** are handled server-side by Gmail and only available in `mail search`. Using them in `mail watch --query` prints a warning and skips the operator.
108
-
109
140
  ### Drafts
110
141
 
111
142
  ```bash
@@ -115,7 +146,7 @@ zele draft send <draft-id>
115
146
  zele draft delete <draft-id>
116
147
  ```
117
148
 
118
- ### Labels
149
+ ### Labels (Google only)
119
150
 
120
151
  ```bash
121
152
  zele label list
@@ -124,7 +155,13 @@ zele label create <name>
124
155
  zele label delete <label-id>
125
156
  ```
126
157
 
127
- ### Calendar
158
+ ### Filters (Google only)
159
+
160
+ ```bash
161
+ zele mail filter list
162
+ ```
163
+
164
+ ### Calendar (Google only)
128
165
 
129
166
  ```bash
130
167
  zele cal list # list calendars
@@ -172,6 +209,24 @@ zele profile # show account info
172
209
 
173
210
  All commands support `--account <email>` to filter by account. Without it, commands fetch from all accounts and merge results.
174
211
 
212
+ Google and IMAP/SMTP accounts work side by side — `mail list` merges results from both. Google-only features (labels, filters, calendar) show a helpful error when used with IMAP accounts.
213
+
214
+ ### Feature compatibility
215
+
216
+ | Feature | Google | IMAP/SMTP |
217
+ |---|---|---|
218
+ | List, read, search emails | yes | yes |
219
+ | Send, reply, forward | yes | yes (requires SMTP) |
220
+ | Star, archive, trash, mark read | yes | yes |
221
+ | Drafts | yes | yes |
222
+ | Attachments | yes | yes |
223
+ | Watch for new emails | yes | yes |
224
+ | Date/sender/subject filters | yes | yes |
225
+ | Labels | yes | no (IMAP uses folders) |
226
+ | Filters | yes | no |
227
+ | Calendar | yes | no |
228
+ | Gmail search operators | full | subset (see table above) |
229
+
175
230
  ## Output
176
231
 
177
232
  All structured data is output as YAML. In TTY mode, keys are colored for readability. Pipe output to other tools for scripting.
@@ -46,6 +46,10 @@ declare const ApiError_base: errore.FactoryTaggedErrorClass<"ApiError", "API cal
46
46
  /** Returned when a non-auth, non-ratelimit API call fails. */
47
47
  export declare class ApiError extends ApiError_base {
48
48
  }
49
+ declare const UnsupportedError_base: errore.FactoryTaggedErrorClass<"UnsupportedError", "$feature is not available for $accountType accounts. $hint", Error>;
50
+ /** Returned when a command requires a capability (e.g. gmail labels) that the account doesn't support. */
51
+ export declare class UnsupportedError extends UnsupportedError_base {
52
+ }
49
53
  /** Detect auth-like errors from underlying libraries (tsdav string errors, googleapis structured errors).
50
54
  * Used inside clients to decide whether to return an AuthError.
51
55
  * NOTE: String matching here is intentional — this is the boundary layer that converts
package/dist/api-utils.js CHANGED
@@ -101,6 +101,12 @@ export class ApiError extends errore.createTaggedError({
101
101
  message: 'API call failed: $reason',
102
102
  }) {
103
103
  }
104
+ /** Returned when a command requires a capability (e.g. gmail labels) that the account doesn't support. */
105
+ export class UnsupportedError extends errore.createTaggedError({
106
+ name: 'UnsupportedError',
107
+ message: '$feature is not available for $accountType accounts. $hint',
108
+ }) {
109
+ }
104
110
  /** Detect auth-like errors from underlying libraries (tsdav string errors, googleapis structured errors).
105
111
  * Used inside clients to decide whether to return an AuthError.
106
112
  * NOTE: String matching here is intentional — this is the boundary layer that converts
@@ -1 +1 @@
1
- {"version":3,"file":"api-utils.js","sourceRoot":"","sources":["../src/api-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-utils.js","sourceRoot":"","sources":["../src/api-utils.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts CHANGED
@@ -1,6 +1,32 @@
1
1
  import { OAuth2Client } from 'google-auth-library';
2
2
  import { GmailClient } from './gmail-client.js';
3
3
  import { CalendarClient } from './calendar-client.js';
4
+ import { ImapSmtpClient } from './imap-smtp-client.js';
5
+ export type AccountType = 'google' | 'imap_smtp';
6
+ export declare const IMAP_SMTP_APP_ID: "imap_smtp";
7
+ export interface ImapCredentials {
8
+ host: string;
9
+ port: number;
10
+ user: string;
11
+ password: string;
12
+ tls: boolean;
13
+ }
14
+ export interface SmtpCredentials {
15
+ host: string;
16
+ port: number;
17
+ user: string;
18
+ password: string;
19
+ tls: boolean;
20
+ }
21
+ /** Stored in the `tokens` column for imap_smtp accounts. */
22
+ export interface ImapSmtpCredentials {
23
+ imap?: ImapCredentials;
24
+ smtp?: SmtpCredentials;
25
+ }
26
+ /** Capabilities an account can have. */
27
+ export type AccountCapability = 'gmail' | 'calendar' | 'smtp' | 'imap';
28
+ export declare function parseCapabilities(raw: string): AccountCapability[];
29
+ export declare function hasCapability(capabilities: string | AccountCapability[], cap: AccountCapability): boolean;
4
30
  /**
5
31
  * Create an OAuth2Client. If appId is provided, looks up the matching
6
32
  * client credentials from OAUTH_CLIENTS by client ID. Falls back to
@@ -10,6 +36,8 @@ export declare function createOAuth2Client(appId?: string): OAuth2Client;
10
36
  export interface AccountId {
11
37
  email: string;
12
38
  appId: string;
39
+ accountType: AccountType;
40
+ capabilities: AccountCapability[];
13
41
  }
14
42
  interface BrowserAuthOptions {
15
43
  openBrowser?: boolean;
@@ -25,28 +53,60 @@ export declare function login(appId?: string, options?: BrowserAuthOptions): Pro
25
53
  appId: string;
26
54
  client: GmailClient;
27
55
  } | Error>;
56
+ export interface LoginImapOptions {
57
+ email: string;
58
+ imapHost: string;
59
+ imapPort?: number;
60
+ smtpHost?: string;
61
+ smtpPort?: number;
62
+ password?: string;
63
+ imapUser?: string;
64
+ imapPassword?: string;
65
+ smtpUser?: string;
66
+ smtpPassword?: string;
67
+ tls?: boolean;
68
+ }
69
+ /**
70
+ * Login with IMAP/SMTP credentials. Tests connections before saving.
71
+ * Returns an error value if validation or connection test fails.
72
+ */
73
+ export declare function loginImap(options: LoginImapOptions): Promise<{
74
+ email: string;
75
+ appId: string;
76
+ } | Error>;
28
77
  export declare function logout(email: string): Promise<void | Error>;
29
78
  export declare function listAccounts(): Promise<AccountId[]>;
79
+ /** Entry returned by getClients — client is GmailClient for Google accounts, ImapSmtpClient for IMAP/SMTP. */
80
+ export interface ClientEntry {
81
+ email: string;
82
+ appId: string;
83
+ accountType: AccountType;
84
+ capabilities: AccountCapability[];
85
+ client: GmailClient | ImapSmtpClient;
86
+ }
30
87
  /**
31
- * Get authenticated GmailClient instances for all accounts (or filtered by email list).
88
+ * Get authenticated client instances for all accounts (or filtered by email list).
89
+ * Returns GmailClient for Google accounts and ImapSmtpClient for IMAP/SMTP accounts.
32
90
  * If no accounts are registered, throws with a helpful message.
33
91
  */
34
- export declare function getClients(accounts?: string[]): Promise<Array<{
35
- email: string;
36
- appId: string;
37
- client: GmailClient;
38
- }>>;
92
+ export declare function getClients(accounts?: string[]): Promise<ClientEntry[]>;
39
93
  /**
40
- * Get a single authenticated GmailClient. Errors if multiple accounts exist
94
+ * Get a single authenticated client. Errors if multiple accounts exist
41
95
  * and no --account filter was provided.
42
96
  */
43
- export declare function getClient(accounts?: string[]): Promise<{
97
+ export declare function getClient(accounts?: string[]): Promise<ClientEntry>;
98
+ /**
99
+ * Get a single authenticated GmailClient. Errors if account is not a Google account.
100
+ * Use this for commands that require Gmail-specific features.
101
+ */
102
+ export declare function getGmailClient(accounts?: string[]): Promise<{
44
103
  email: string;
45
104
  appId: string;
46
105
  client: GmailClient;
47
106
  }>;
48
107
  /**
49
- * Get authenticated CalendarClient instances for all accounts (or filtered by email list).
108
+ * Get authenticated CalendarClient instances for Google accounts only.
109
+ * IMAP/SMTP accounts are silently skipped (calendar requires Google OAuth).
50
110
  */
51
111
  export declare function getCalendarClients(accounts?: string[]): Promise<Array<{
52
112
  email: string;
@@ -65,6 +125,8 @@ export declare function getCalendarClient(accounts?: string[]): Promise<{
65
125
  export interface AuthStatus {
66
126
  email: string;
67
127
  appId: string;
128
+ accountType: AccountType;
129
+ capabilities: AccountCapability[];
68
130
  expiresAt?: Date;
69
131
  }
70
132
  export declare function getAuthStatuses(): Promise<AuthStatus[]>;
package/dist/auth.js CHANGED
@@ -15,6 +15,18 @@ import { getPrisma } from './db.js';
15
15
  import { GmailClient } from './gmail-client.js';
16
16
  import { CalendarClient } from './calendar-client.js';
17
17
  import * as errore from 'errore';
18
+ import { AuthError, UnsupportedError } from './api-utils.js';
19
+ import { ImapSmtpClient } from './imap-smtp-client.js';
20
+ export const IMAP_SMTP_APP_ID = 'imap_smtp';
21
+ export function parseCapabilities(raw) {
22
+ if (!raw)
23
+ return [];
24
+ return raw.split(',').map((s) => s.trim()).filter(Boolean);
25
+ }
26
+ export function hasCapability(capabilities, cap) {
27
+ const list = typeof capabilities === 'string' ? parseCapabilities(capabilities) : capabilities;
28
+ return list.includes(cap);
29
+ }
18
30
  // ---------------------------------------------------------------------------
19
31
  // Known open-source Google OAuth clients (Desktop app type).
20
32
  // All support localhost + OOB redirects. All have Gmail, Calendar, Drive,
@@ -449,18 +461,21 @@ export async function login(appId, options) {
449
461
  const email = profile.emailAddress;
450
462
  // Upsert account in DB
451
463
  const prisma = await getPrisma();
464
+ const googleCapabilities = 'gmail,calendar,smtp';
452
465
  const upsertResult = await errore.tryAsync({
453
466
  try: () => prisma.account.upsert({
454
467
  where: { email_appId: { email, appId: resolved.clientId } },
455
468
  create: {
456
469
  email,
457
470
  appId: resolved.clientId,
471
+ accountType: 'google',
472
+ capabilities: googleCapabilities,
458
473
  accountStatus: 'active',
459
474
  tokens: JSON.stringify(tokens),
460
475
  createdAt: new Date(),
461
476
  updatedAt: new Date(),
462
477
  },
463
- update: { tokens: JSON.stringify(tokens), updatedAt: new Date() },
478
+ update: { tokens: JSON.stringify(tokens), capabilities: googleCapabilities, updatedAt: new Date() },
464
479
  }),
465
480
  catch: (err) => new Error(`Failed to save account ${email}`, { cause: err }),
466
481
  });
@@ -468,6 +483,100 @@ export async function login(appId, options) {
468
483
  return upsertResult;
469
484
  return { email, appId: resolved.clientId, client };
470
485
  }
486
+ /**
487
+ * Login with IMAP/SMTP credentials. Tests connections before saving.
488
+ * Returns an error value if validation or connection test fails.
489
+ */
490
+ export async function loginImap(options) {
491
+ const { email, imapHost, imapPort = 993, smtpHost, smtpPort = 465, password, imapUser, imapPassword, smtpUser, smtpPassword, tls = true, } = options;
492
+ const imapPass = imapPassword ?? password;
493
+ if (!imapPass) {
494
+ return new Error('IMAP password is required (--password or --imap-password)');
495
+ }
496
+ const credentials = {
497
+ imap: {
498
+ host: imapHost,
499
+ port: imapPort,
500
+ user: imapUser ?? email,
501
+ password: imapPass,
502
+ tls,
503
+ },
504
+ };
505
+ // Test IMAP connection
506
+ const { ImapFlow } = await import('imapflow');
507
+ const testClient = new ImapFlow({
508
+ host: imapHost,
509
+ port: imapPort,
510
+ secure: tls,
511
+ auth: { user: imapUser ?? email, pass: imapPass },
512
+ logger: false,
513
+ });
514
+ const imapTest = await errore.tryAsync({
515
+ try: async () => {
516
+ await testClient.connect();
517
+ await testClient.logout();
518
+ },
519
+ catch: (err) => new AuthError({ email, reason: `IMAP connection failed: ${String(err)}` }),
520
+ });
521
+ if (imapTest instanceof Error)
522
+ return imapTest;
523
+ // Configure SMTP if provided
524
+ const capabilities = ['imap'];
525
+ if (smtpHost) {
526
+ const smtpPass = smtpPassword ?? password;
527
+ if (!smtpPass) {
528
+ return new Error('SMTP password is required when --smtp-host is provided (--password or --smtp-password)');
529
+ }
530
+ credentials.smtp = {
531
+ host: smtpHost,
532
+ port: smtpPort,
533
+ user: smtpUser ?? email,
534
+ password: smtpPass,
535
+ tls: smtpPort === 465,
536
+ };
537
+ // Test SMTP connection
538
+ const nodemailer = await import('nodemailer');
539
+ const transporter = nodemailer.default.createTransport({
540
+ host: smtpHost,
541
+ port: smtpPort,
542
+ secure: smtpPort === 465,
543
+ auth: { user: smtpUser ?? email, pass: smtpPass },
544
+ });
545
+ const smtpTest = await errore.tryAsync({
546
+ try: () => transporter.verify(),
547
+ catch: (err) => new AuthError({ email, reason: `SMTP connection failed: ${String(err)}` }),
548
+ });
549
+ if (smtpTest instanceof Error)
550
+ return smtpTest;
551
+ capabilities.push('smtp');
552
+ }
553
+ // Save to DB
554
+ const prisma = await getPrisma();
555
+ const upsertResult = await errore.tryAsync({
556
+ try: () => prisma.account.upsert({
557
+ where: { email_appId: { email, appId: IMAP_SMTP_APP_ID } },
558
+ create: {
559
+ email,
560
+ appId: IMAP_SMTP_APP_ID,
561
+ accountType: 'imap_smtp',
562
+ capabilities: capabilities.join(','),
563
+ accountStatus: 'active',
564
+ tokens: JSON.stringify(credentials),
565
+ createdAt: new Date(),
566
+ updatedAt: new Date(),
567
+ },
568
+ update: {
569
+ capabilities: capabilities.join(','),
570
+ tokens: JSON.stringify(credentials),
571
+ updatedAt: new Date(),
572
+ },
573
+ }),
574
+ catch: (err) => new Error(`Failed to save account ${email}`, { cause: err }),
575
+ });
576
+ if (upsertResult instanceof Error)
577
+ return upsertResult;
578
+ return { email, appId: IMAP_SMTP_APP_ID };
579
+ }
471
580
  // ---------------------------------------------------------------------------
472
581
  // Logout: remove account from DB
473
582
  // ---------------------------------------------------------------------------
@@ -486,8 +595,13 @@ export async function logout(email) {
486
595
  // ---------------------------------------------------------------------------
487
596
  export async function listAccounts() {
488
597
  const prisma = await getPrisma();
489
- const rows = await prisma.account.findMany({ select: { email: true, appId: true } });
490
- return rows.map((r) => ({ email: r.email, appId: r.appId }));
598
+ const rows = await prisma.account.findMany({ select: { email: true, appId: true, accountType: true, capabilities: true } });
599
+ return rows.map((r) => ({
600
+ email: r.email,
601
+ appId: r.appId,
602
+ accountType: r.accountType,
603
+ capabilities: parseCapabilities(r.capabilities),
604
+ }));
491
605
  }
492
606
  // ---------------------------------------------------------------------------
493
607
  // Get authenticated clients
@@ -523,7 +637,8 @@ async function authenticateAccount(account) {
523
637
  return oauth2Client;
524
638
  }
525
639
  /**
526
- * Get authenticated GmailClient instances for all accounts (or filtered by email list).
640
+ * Get authenticated client instances for all accounts (or filtered by email list).
641
+ * Returns GmailClient for Google accounts and ImapSmtpClient for IMAP/SMTP accounts.
527
642
  * If no accounts are registered, throws with a helpful message.
528
643
  */
529
644
  export async function getClients(accounts) {
@@ -538,14 +653,37 @@ export async function getClients(accounts) {
538
653
  const available = allAccounts.map((a) => a.email).join(', ');
539
654
  throw new Error(`No matching accounts. Available: ${available}`);
540
655
  }
656
+ const prisma = await getPrisma();
541
657
  const results = await Promise.all(filtered.map(async (account) => {
658
+ if (account.accountType === 'imap_smtp') {
659
+ const row = await prisma.account.findUnique({
660
+ where: { email_appId: { email: account.email, appId: account.appId } },
661
+ });
662
+ if (!row)
663
+ throw new Error(`No account found for ${account.email}. Run: zele login`);
664
+ const credentials = JSON.parse(row.tokens);
665
+ return {
666
+ email: account.email,
667
+ appId: account.appId,
668
+ accountType: 'imap_smtp',
669
+ capabilities: account.capabilities,
670
+ client: new ImapSmtpClient({ credentials, account }),
671
+ };
672
+ }
673
+ // Google account
542
674
  const auth = await authenticateAccount(account);
543
- return { email: account.email, appId: account.appId, client: new GmailClient({ auth, account }) };
675
+ return {
676
+ email: account.email,
677
+ appId: account.appId,
678
+ accountType: 'google',
679
+ capabilities: account.capabilities,
680
+ client: new GmailClient({ auth, account }),
681
+ };
544
682
  }));
545
683
  return results;
546
684
  }
547
685
  /**
548
- * Get a single authenticated GmailClient. Errors if multiple accounts exist
686
+ * Get a single authenticated client. Errors if multiple accounts exist
549
687
  * and no --account filter was provided.
550
688
  */
551
689
  export async function getClient(accounts) {
@@ -556,11 +694,27 @@ export async function getClient(accounts) {
556
694
  const emails = clients.map((c) => c.email).join('\n ');
557
695
  throw new Error(`Multiple accounts matched. Specify --account:\n ${emails}`);
558
696
  }
697
+ /**
698
+ * Get a single authenticated GmailClient. Errors if account is not a Google account.
699
+ * Use this for commands that require Gmail-specific features.
700
+ */
701
+ export async function getGmailClient(accounts) {
702
+ const entry = await getClient(accounts);
703
+ if (entry.accountType !== 'google') {
704
+ throw new UnsupportedError({
705
+ feature: 'This command',
706
+ accountType: 'IMAP/SMTP',
707
+ hint: 'It requires a Google account.',
708
+ });
709
+ }
710
+ return { email: entry.email, appId: entry.appId, client: entry.client };
711
+ }
559
712
  // ---------------------------------------------------------------------------
560
713
  // Calendar client helpers
561
714
  // ---------------------------------------------------------------------------
562
715
  /**
563
- * Get authenticated CalendarClient instances for all accounts (or filtered by email list).
716
+ * Get authenticated CalendarClient instances for Google accounts only.
717
+ * IMAP/SMTP accounts are silently skipped (calendar requires Google OAuth).
564
718
  */
565
719
  export async function getCalendarClients(accounts) {
566
720
  const allAccounts = await listAccounts();
@@ -574,7 +728,16 @@ export async function getCalendarClients(accounts) {
574
728
  const available = allAccounts.map((a) => a.email).join(', ');
575
729
  throw new Error(`No matching accounts. Available: ${available}`);
576
730
  }
577
- const results = await Promise.all(filtered.map(async (account) => {
731
+ // Only Google accounts support calendar
732
+ const googleAccounts = filtered.filter((a) => a.accountType === 'google');
733
+ if (googleAccounts.length === 0) {
734
+ throw new UnsupportedError({
735
+ feature: 'Calendar',
736
+ accountType: 'IMAP/SMTP',
737
+ hint: 'Calendar requires a Google account.',
738
+ });
739
+ }
740
+ const results = await Promise.all(googleAccounts.map(async (account) => {
578
741
  const auth = await authenticateAccount(account);
579
742
  const { token } = await auth.getAccessToken();
580
743
  if (!token)
@@ -599,11 +762,24 @@ export async function getAuthStatuses() {
599
762
  const prisma = await getPrisma();
600
763
  const rows = await prisma.account.findMany();
601
764
  return rows.map((row) => {
602
- const tokens = JSON.parse(row.tokens);
765
+ const accountType = row.accountType;
766
+ const capabilities = parseCapabilities(row.capabilities);
767
+ if (accountType === 'google') {
768
+ const tokens = JSON.parse(row.tokens);
769
+ return {
770
+ email: row.email,
771
+ appId: row.appId,
772
+ accountType,
773
+ capabilities,
774
+ expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : undefined,
775
+ };
776
+ }
777
+ // IMAP/SMTP — no expiry
603
778
  return {
604
779
  email: row.email,
605
780
  appId: row.appId,
606
- expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : undefined,
781
+ accountType,
782
+ capabilities,
607
783
  };
608
784
  });
609
785
  }