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.
- package/README.md +91 -36
- package/dist/api-utils.d.ts +4 -0
- package/dist/api-utils.js +6 -0
- package/dist/api-utils.js.map +1 -1
- package/dist/auth.d.ts +71 -9
- package/dist/auth.js +186 -10
- package/dist/auth.js.map +1 -1
- package/dist/commands/attachment.js +2 -0
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.js +104 -6
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/draft.js +7 -1
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/filter.js +7 -2
- package/dist/commands/filter.js.map +1 -1
- package/dist/commands/label.js +19 -9
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.js +49 -22
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/profile.js +25 -18
- package/dist/commands/profile.js.map +1 -1
- package/dist/db.js +24 -0
- package/dist/db.js.map +1 -1
- package/dist/generated/internal/class.js +2 -2
- package/dist/generated/internal/class.js.map +1 -1
- package/dist/generated/internal/prismaNamespace.d.ts +2 -0
- package/dist/generated/internal/prismaNamespace.js +2 -0
- package/dist/generated/internal/prismaNamespace.js.map +1 -1
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/generated/models/Account.d.ts +97 -1
- package/dist/gmail-client.d.ts +14 -0
- package/dist/gmail-client.js +46 -0
- package/dist/gmail-client.js.map +1 -1
- package/dist/imap-smtp-client.d.ts +235 -0
- package/dist/imap-smtp-client.js +1225 -0
- package/dist/imap-smtp-client.js.map +1 -0
- package/dist/mail-tui.js.map +1 -1
- package/package.json +5 -2
- package/schema.prisma +7 -5
- package/skills/zele/SKILL.md +50 -21
- package/src/api-utils.ts +6 -0
- package/src/auth.ts +282 -14
- package/src/commands/attachment.ts +1 -0
- package/src/commands/auth-cmd.ts +112 -6
- package/src/commands/draft.ts +5 -1
- package/src/commands/filter.ts +9 -3
- package/src/commands/label.ts +22 -11
- package/src/commands/mail-actions.ts +2 -1
- package/src/commands/mail.ts +52 -22
- package/src/commands/profile.ts +27 -17
- package/src/db.ts +28 -0
- package/src/generated/internal/class.ts +2 -2
- package/src/generated/internal/prismaNamespace.ts +2 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
- package/src/generated/models/Account.ts +97 -1
- package/src/gmail-client.test.ts +155 -2
- package/src/gmail-client.ts +65 -0
- package/src/imap-smtp-client.ts +1381 -0
- package/src/mail-tui.tsx +2 -1
- package/src/schema.sql +2 -0
package/src/auth.ts
CHANGED
|
@@ -16,7 +16,51 @@ import { getPrisma } from './db.js'
|
|
|
16
16
|
import { GmailClient } from './gmail-client.js'
|
|
17
17
|
import { CalendarClient } from './calendar-client.js'
|
|
18
18
|
import * as errore from 'errore'
|
|
19
|
-
import { AuthError } from './api-utils.js'
|
|
19
|
+
import { AuthError, ApiError, UnsupportedError } from './api-utils.js'
|
|
20
|
+
import { ImapSmtpClient } from './imap-smtp-client.js'
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Account types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type AccountType = 'google' | 'imap_smtp'
|
|
27
|
+
|
|
28
|
+
export const IMAP_SMTP_APP_ID = 'imap_smtp' as const
|
|
29
|
+
|
|
30
|
+
export interface ImapCredentials {
|
|
31
|
+
host: string
|
|
32
|
+
port: number
|
|
33
|
+
user: string
|
|
34
|
+
password: string
|
|
35
|
+
tls: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SmtpCredentials {
|
|
39
|
+
host: string
|
|
40
|
+
port: number
|
|
41
|
+
user: string
|
|
42
|
+
password: string
|
|
43
|
+
tls: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Stored in the `tokens` column for imap_smtp accounts. */
|
|
47
|
+
export interface ImapSmtpCredentials {
|
|
48
|
+
imap?: ImapCredentials
|
|
49
|
+
smtp?: SmtpCredentials
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Capabilities an account can have. */
|
|
53
|
+
export type AccountCapability = 'gmail' | 'calendar' | 'smtp' | 'imap'
|
|
54
|
+
|
|
55
|
+
export function parseCapabilities(raw: string): AccountCapability[] {
|
|
56
|
+
if (!raw) return []
|
|
57
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean) as AccountCapability[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hasCapability(capabilities: string | AccountCapability[], cap: AccountCapability): boolean {
|
|
61
|
+
const list = typeof capabilities === 'string' ? parseCapabilities(capabilities) : capabilities
|
|
62
|
+
return list.includes(cap)
|
|
63
|
+
}
|
|
20
64
|
|
|
21
65
|
// ---------------------------------------------------------------------------
|
|
22
66
|
// Known open-source Google OAuth clients (Desktop app type).
|
|
@@ -123,6 +167,8 @@ export function createOAuth2Client(appId?: string): OAuth2Client {
|
|
|
123
167
|
export interface AccountId {
|
|
124
168
|
email: string
|
|
125
169
|
appId: string
|
|
170
|
+
accountType: AccountType
|
|
171
|
+
capabilities: AccountCapability[]
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
// ---------------------------------------------------------------------------
|
|
@@ -530,6 +576,7 @@ export async function login(
|
|
|
530
576
|
|
|
531
577
|
// Upsert account in DB
|
|
532
578
|
const prisma = await getPrisma()
|
|
579
|
+
const googleCapabilities = 'gmail,calendar,smtp'
|
|
533
580
|
const upsertResult = await errore.tryAsync({
|
|
534
581
|
try: () =>
|
|
535
582
|
prisma.account.upsert({
|
|
@@ -537,12 +584,14 @@ export async function login(
|
|
|
537
584
|
create: {
|
|
538
585
|
email,
|
|
539
586
|
appId: resolved.clientId,
|
|
587
|
+
accountType: 'google',
|
|
588
|
+
capabilities: googleCapabilities,
|
|
540
589
|
accountStatus: 'active',
|
|
541
590
|
tokens: JSON.stringify(tokens),
|
|
542
591
|
createdAt: new Date(),
|
|
543
592
|
updatedAt: new Date(),
|
|
544
593
|
},
|
|
545
|
-
update: { tokens: JSON.stringify(tokens), updatedAt: new Date() },
|
|
594
|
+
update: { tokens: JSON.stringify(tokens), capabilities: googleCapabilities, updatedAt: new Date() },
|
|
546
595
|
}),
|
|
547
596
|
catch: (err) => new Error(`Failed to save account ${email}`, { cause: err }),
|
|
548
597
|
})
|
|
@@ -551,6 +600,143 @@ export async function login(
|
|
|
551
600
|
return { email, appId: resolved.clientId, client }
|
|
552
601
|
}
|
|
553
602
|
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Login: IMAP/SMTP credentials → save to DB
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
export interface LoginImapOptions {
|
|
608
|
+
email: string
|
|
609
|
+
imapHost: string
|
|
610
|
+
imapPort?: number
|
|
611
|
+
smtpHost?: string
|
|
612
|
+
smtpPort?: number
|
|
613
|
+
password?: string
|
|
614
|
+
imapUser?: string
|
|
615
|
+
imapPassword?: string
|
|
616
|
+
smtpUser?: string
|
|
617
|
+
smtpPassword?: string
|
|
618
|
+
tls?: boolean
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Login with IMAP/SMTP credentials. Tests connections before saving.
|
|
623
|
+
* Returns an error value if validation or connection test fails.
|
|
624
|
+
*/
|
|
625
|
+
export async function loginImap(
|
|
626
|
+
options: LoginImapOptions,
|
|
627
|
+
): Promise<{ email: string; appId: string } | Error> {
|
|
628
|
+
const {
|
|
629
|
+
email,
|
|
630
|
+
imapHost,
|
|
631
|
+
imapPort = 993,
|
|
632
|
+
smtpHost,
|
|
633
|
+
smtpPort = 465,
|
|
634
|
+
password,
|
|
635
|
+
imapUser,
|
|
636
|
+
imapPassword,
|
|
637
|
+
smtpUser,
|
|
638
|
+
smtpPassword,
|
|
639
|
+
tls = true,
|
|
640
|
+
} = options
|
|
641
|
+
|
|
642
|
+
const imapPass = imapPassword ?? password
|
|
643
|
+
if (!imapPass) {
|
|
644
|
+
return new Error('IMAP password is required (--password or --imap-password)')
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const credentials: ImapSmtpCredentials = {
|
|
648
|
+
imap: {
|
|
649
|
+
host: imapHost,
|
|
650
|
+
port: imapPort,
|
|
651
|
+
user: imapUser ?? email,
|
|
652
|
+
password: imapPass,
|
|
653
|
+
tls,
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Test IMAP connection
|
|
658
|
+
const { ImapFlow } = await import('imapflow')
|
|
659
|
+
const testClient = new ImapFlow({
|
|
660
|
+
host: imapHost,
|
|
661
|
+
port: imapPort,
|
|
662
|
+
secure: tls,
|
|
663
|
+
auth: { user: imapUser ?? email, pass: imapPass },
|
|
664
|
+
logger: false,
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
const imapTest = await errore.tryAsync({
|
|
668
|
+
try: async () => {
|
|
669
|
+
await testClient.connect()
|
|
670
|
+
await testClient.logout()
|
|
671
|
+
},
|
|
672
|
+
catch: (err) => new AuthError({ email, reason: `IMAP connection failed: ${String(err)}` }),
|
|
673
|
+
})
|
|
674
|
+
if (imapTest instanceof Error) return imapTest
|
|
675
|
+
|
|
676
|
+
// Configure SMTP if provided
|
|
677
|
+
const capabilities: AccountCapability[] = ['imap']
|
|
678
|
+
|
|
679
|
+
if (smtpHost) {
|
|
680
|
+
const smtpPass = smtpPassword ?? password
|
|
681
|
+
if (!smtpPass) {
|
|
682
|
+
return new Error('SMTP password is required when --smtp-host is provided (--password or --smtp-password)')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
credentials.smtp = {
|
|
686
|
+
host: smtpHost,
|
|
687
|
+
port: smtpPort,
|
|
688
|
+
user: smtpUser ?? email,
|
|
689
|
+
password: smtpPass,
|
|
690
|
+
tls: smtpPort === 465,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Test SMTP connection
|
|
694
|
+
const nodemailer = await import('nodemailer')
|
|
695
|
+
const transporter = nodemailer.default.createTransport({
|
|
696
|
+
host: smtpHost,
|
|
697
|
+
port: smtpPort,
|
|
698
|
+
secure: smtpPort === 465,
|
|
699
|
+
auth: { user: smtpUser ?? email, pass: smtpPass },
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
const smtpTest = await errore.tryAsync({
|
|
703
|
+
try: () => transporter.verify(),
|
|
704
|
+
catch: (err) => new AuthError({ email, reason: `SMTP connection failed: ${String(err)}` }),
|
|
705
|
+
})
|
|
706
|
+
if (smtpTest instanceof Error) return smtpTest
|
|
707
|
+
|
|
708
|
+
capabilities.push('smtp')
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Save to DB
|
|
712
|
+
const prisma = await getPrisma()
|
|
713
|
+
const upsertResult = await errore.tryAsync({
|
|
714
|
+
try: () =>
|
|
715
|
+
prisma.account.upsert({
|
|
716
|
+
where: { email_appId: { email, appId: IMAP_SMTP_APP_ID } },
|
|
717
|
+
create: {
|
|
718
|
+
email,
|
|
719
|
+
appId: IMAP_SMTP_APP_ID,
|
|
720
|
+
accountType: 'imap_smtp',
|
|
721
|
+
capabilities: capabilities.join(','),
|
|
722
|
+
accountStatus: 'active',
|
|
723
|
+
tokens: JSON.stringify(credentials),
|
|
724
|
+
createdAt: new Date(),
|
|
725
|
+
updatedAt: new Date(),
|
|
726
|
+
},
|
|
727
|
+
update: {
|
|
728
|
+
capabilities: capabilities.join(','),
|
|
729
|
+
tokens: JSON.stringify(credentials),
|
|
730
|
+
updatedAt: new Date(),
|
|
731
|
+
},
|
|
732
|
+
}),
|
|
733
|
+
catch: (err) => new Error(`Failed to save account ${email}`, { cause: err }),
|
|
734
|
+
})
|
|
735
|
+
if (upsertResult instanceof Error) return upsertResult
|
|
736
|
+
|
|
737
|
+
return { email, appId: IMAP_SMTP_APP_ID }
|
|
738
|
+
}
|
|
739
|
+
|
|
554
740
|
// ---------------------------------------------------------------------------
|
|
555
741
|
// Logout: remove account from DB
|
|
556
742
|
// ---------------------------------------------------------------------------
|
|
@@ -571,8 +757,13 @@ export async function logout(email: string): Promise<void | Error> {
|
|
|
571
757
|
|
|
572
758
|
export async function listAccounts(): Promise<AccountId[]> {
|
|
573
759
|
const prisma = await getPrisma()
|
|
574
|
-
const rows = await prisma.account.findMany({ select: { email: true, appId: true } })
|
|
575
|
-
return rows.map((r) => ({
|
|
760
|
+
const rows = await prisma.account.findMany({ select: { email: true, appId: true, accountType: true, capabilities: true } })
|
|
761
|
+
return rows.map((r) => ({
|
|
762
|
+
email: r.email,
|
|
763
|
+
appId: r.appId,
|
|
764
|
+
accountType: r.accountType as AccountType,
|
|
765
|
+
capabilities: parseCapabilities(r.capabilities),
|
|
766
|
+
}))
|
|
576
767
|
}
|
|
577
768
|
|
|
578
769
|
// ---------------------------------------------------------------------------
|
|
@@ -613,13 +804,23 @@ async function authenticateAccount(account: AccountId): Promise<OAuth2Client> {
|
|
|
613
804
|
return oauth2Client
|
|
614
805
|
}
|
|
615
806
|
|
|
807
|
+
/** Entry returned by getClients — client is GmailClient for Google accounts, ImapSmtpClient for IMAP/SMTP. */
|
|
808
|
+
export interface ClientEntry {
|
|
809
|
+
email: string
|
|
810
|
+
appId: string
|
|
811
|
+
accountType: AccountType
|
|
812
|
+
capabilities: AccountCapability[]
|
|
813
|
+
client: GmailClient | ImapSmtpClient
|
|
814
|
+
}
|
|
815
|
+
|
|
616
816
|
/**
|
|
617
|
-
* Get authenticated
|
|
817
|
+
* Get authenticated client instances for all accounts (or filtered by email list).
|
|
818
|
+
* Returns GmailClient for Google accounts and ImapSmtpClient for IMAP/SMTP accounts.
|
|
618
819
|
* If no accounts are registered, throws with a helpful message.
|
|
619
820
|
*/
|
|
620
821
|
export async function getClients(
|
|
621
822
|
accounts?: string[],
|
|
622
|
-
): Promise<
|
|
823
|
+
): Promise<ClientEntry[]> {
|
|
623
824
|
const allAccounts = await listAccounts()
|
|
624
825
|
if (allAccounts.length === 0) {
|
|
625
826
|
throw new Error('No accounts registered. Run: zele login')
|
|
@@ -634,10 +835,33 @@ export async function getClients(
|
|
|
634
835
|
throw new Error(`No matching accounts. Available: ${available}`)
|
|
635
836
|
}
|
|
636
837
|
|
|
838
|
+
const prisma = await getPrisma()
|
|
637
839
|
const results = await Promise.all(
|
|
638
|
-
filtered.map(async (account) => {
|
|
840
|
+
filtered.map(async (account): Promise<ClientEntry> => {
|
|
841
|
+
if (account.accountType === 'imap_smtp') {
|
|
842
|
+
const row = await prisma.account.findUnique({
|
|
843
|
+
where: { email_appId: { email: account.email, appId: account.appId } },
|
|
844
|
+
})
|
|
845
|
+
if (!row) throw new Error(`No account found for ${account.email}. Run: zele login`)
|
|
846
|
+
const credentials: ImapSmtpCredentials = JSON.parse(row.tokens)
|
|
847
|
+
return {
|
|
848
|
+
email: account.email,
|
|
849
|
+
appId: account.appId,
|
|
850
|
+
accountType: 'imap_smtp',
|
|
851
|
+
capabilities: account.capabilities,
|
|
852
|
+
client: new ImapSmtpClient({ credentials, account }),
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Google account
|
|
639
857
|
const auth = await authenticateAccount(account)
|
|
640
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
email: account.email,
|
|
860
|
+
appId: account.appId,
|
|
861
|
+
accountType: 'google',
|
|
862
|
+
capabilities: account.capabilities,
|
|
863
|
+
client: new GmailClient({ auth, account }),
|
|
864
|
+
}
|
|
641
865
|
}),
|
|
642
866
|
)
|
|
643
867
|
|
|
@@ -645,12 +869,12 @@ export async function getClients(
|
|
|
645
869
|
}
|
|
646
870
|
|
|
647
871
|
/**
|
|
648
|
-
* Get a single authenticated
|
|
872
|
+
* Get a single authenticated client. Errors if multiple accounts exist
|
|
649
873
|
* and no --account filter was provided.
|
|
650
874
|
*/
|
|
651
875
|
export async function getClient(
|
|
652
876
|
accounts?: string[],
|
|
653
|
-
): Promise<
|
|
877
|
+
): Promise<ClientEntry> {
|
|
654
878
|
const clients = await getClients(accounts)
|
|
655
879
|
if (clients.length === 1) {
|
|
656
880
|
return clients[0]!
|
|
@@ -662,12 +886,31 @@ export async function getClient(
|
|
|
662
886
|
)
|
|
663
887
|
}
|
|
664
888
|
|
|
889
|
+
/**
|
|
890
|
+
* Get a single authenticated GmailClient. Errors if account is not a Google account.
|
|
891
|
+
* Use this for commands that require Gmail-specific features.
|
|
892
|
+
*/
|
|
893
|
+
export async function getGmailClient(
|
|
894
|
+
accounts?: string[],
|
|
895
|
+
): Promise<{ email: string; appId: string; client: GmailClient }> {
|
|
896
|
+
const entry = await getClient(accounts)
|
|
897
|
+
if (entry.accountType !== 'google') {
|
|
898
|
+
throw new UnsupportedError({
|
|
899
|
+
feature: 'This command',
|
|
900
|
+
accountType: 'IMAP/SMTP',
|
|
901
|
+
hint: 'It requires a Google account.',
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
return { email: entry.email, appId: entry.appId, client: entry.client as GmailClient }
|
|
905
|
+
}
|
|
906
|
+
|
|
665
907
|
// ---------------------------------------------------------------------------
|
|
666
908
|
// Calendar client helpers
|
|
667
909
|
// ---------------------------------------------------------------------------
|
|
668
910
|
|
|
669
911
|
/**
|
|
670
|
-
* Get authenticated CalendarClient instances for
|
|
912
|
+
* Get authenticated CalendarClient instances for Google accounts only.
|
|
913
|
+
* IMAP/SMTP accounts are silently skipped (calendar requires Google OAuth).
|
|
671
914
|
*/
|
|
672
915
|
export async function getCalendarClients(
|
|
673
916
|
accounts?: string[],
|
|
@@ -686,8 +929,18 @@ export async function getCalendarClients(
|
|
|
686
929
|
throw new Error(`No matching accounts. Available: ${available}`)
|
|
687
930
|
}
|
|
688
931
|
|
|
932
|
+
// Only Google accounts support calendar
|
|
933
|
+
const googleAccounts = filtered.filter((a) => a.accountType === 'google')
|
|
934
|
+
if (googleAccounts.length === 0) {
|
|
935
|
+
throw new UnsupportedError({
|
|
936
|
+
feature: 'Calendar',
|
|
937
|
+
accountType: 'IMAP/SMTP',
|
|
938
|
+
hint: 'Calendar requires a Google account.',
|
|
939
|
+
})
|
|
940
|
+
}
|
|
941
|
+
|
|
689
942
|
const results = await Promise.all(
|
|
690
|
-
|
|
943
|
+
googleAccounts.map(async (account) => {
|
|
691
944
|
const auth = await authenticateAccount(account)
|
|
692
945
|
const { token } = await auth.getAccessToken()
|
|
693
946
|
if (!token) throw new Error(`Failed to get access token for ${account.email}`)
|
|
@@ -723,6 +976,8 @@ export async function getCalendarClient(
|
|
|
723
976
|
export interface AuthStatus {
|
|
724
977
|
email: string
|
|
725
978
|
appId: string
|
|
979
|
+
accountType: AccountType
|
|
980
|
+
capabilities: AccountCapability[]
|
|
726
981
|
expiresAt?: Date
|
|
727
982
|
}
|
|
728
983
|
|
|
@@ -731,11 +986,24 @@ export async function getAuthStatuses(): Promise<AuthStatus[]> {
|
|
|
731
986
|
const rows = await prisma.account.findMany()
|
|
732
987
|
|
|
733
988
|
return rows.map((row) => {
|
|
734
|
-
const
|
|
989
|
+
const accountType = row.accountType as AccountType
|
|
990
|
+
const capabilities = parseCapabilities(row.capabilities)
|
|
991
|
+
if (accountType === 'google') {
|
|
992
|
+
const tokens: Credentials = JSON.parse(row.tokens)
|
|
993
|
+
return {
|
|
994
|
+
email: row.email,
|
|
995
|
+
appId: row.appId,
|
|
996
|
+
accountType,
|
|
997
|
+
capabilities,
|
|
998
|
+
expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : undefined,
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// IMAP/SMTP — no expiry
|
|
735
1002
|
return {
|
|
736
1003
|
email: row.email,
|
|
737
1004
|
appId: row.appId,
|
|
738
|
-
|
|
1005
|
+
accountType,
|
|
1006
|
+
capabilities,
|
|
739
1007
|
}
|
|
740
1008
|
})
|
|
741
1009
|
}
|
|
@@ -86,6 +86,7 @@ export function registerAttachmentCommands(cli: Goke) {
|
|
|
86
86
|
|
|
87
87
|
// Download
|
|
88
88
|
const base64Data = await client.getAttachment({ messageId, attachmentId })
|
|
89
|
+
if (base64Data instanceof Error) return handleCommandError(base64Data)
|
|
89
90
|
const buffer = Buffer.from(base64Data, 'base64')
|
|
90
91
|
|
|
91
92
|
// Ensure output directory exists
|
package/src/commands/auth-cmd.ts
CHANGED
|
@@ -1,17 +1,72 @@
|
|
|
1
|
-
// Auth commands: login, logout, whoami.
|
|
2
|
-
// Manages
|
|
1
|
+
// Auth commands: login, login imap, logout, whoami.
|
|
2
|
+
// Manages authentication for zele (Google OAuth and IMAP/SMTP credentials).
|
|
3
3
|
// Supports multiple accounts: login adds accounts, logout removes one.
|
|
4
4
|
|
|
5
5
|
import type { Goke } from 'goke'
|
|
6
|
-
import {
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import pc from 'picocolors'
|
|
8
|
+
import { login, loginImap, logout, listAccounts, getAuthStatuses } from '../auth.js'
|
|
7
9
|
import { closePrisma } from '../db.js'
|
|
8
10
|
import * as out from '../output.js'
|
|
9
11
|
import { handleCommandError } from '../output.js'
|
|
10
12
|
|
|
11
13
|
export function registerAuthCommands(cli: Goke) {
|
|
12
14
|
cli
|
|
13
|
-
.command('login', 'Authenticate with Google (opens browser)
|
|
15
|
+
.command('login', 'Authenticate with Google (opens browser) or show IMAP/SMTP login instructions')
|
|
14
16
|
.action(async () => {
|
|
17
|
+
// In a TTY, ask if they want Google or Other
|
|
18
|
+
if (process.stdin.isTTY) {
|
|
19
|
+
const readline = await import('node:readline')
|
|
20
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
21
|
+
|
|
22
|
+
console.error(pc.bold('\nChoose authentication method:\n'))
|
|
23
|
+
console.error(' ' + pc.cyan('1') + ' Google (opens browser for OAuth)')
|
|
24
|
+
console.error(' ' + pc.cyan('2') + ' Other (IMAP/SMTP with password)\n')
|
|
25
|
+
|
|
26
|
+
const answer = await new Promise<string>((resolve) => {
|
|
27
|
+
rl.question('Enter choice [1]: ', resolve)
|
|
28
|
+
})
|
|
29
|
+
rl.close()
|
|
30
|
+
|
|
31
|
+
const choice = answer.trim() || '1'
|
|
32
|
+
|
|
33
|
+
if (choice === '2') {
|
|
34
|
+
console.error(pc.bold('\nTo add an IMAP/SMTP account, run:\n'))
|
|
35
|
+
console.error(pc.dim(' # Fastmail'))
|
|
36
|
+
console.error(` zele login imap \\`)
|
|
37
|
+
console.error(` --email you@fastmail.com \\`)
|
|
38
|
+
console.error(` --imap-host imap.fastmail.com --imap-port 993 \\`)
|
|
39
|
+
console.error(` --smtp-host smtp.fastmail.com --smtp-port 465 \\`)
|
|
40
|
+
console.error(` --password "your-app-password"`)
|
|
41
|
+
console.error()
|
|
42
|
+
console.error(pc.dim(' # Gmail (app password)'))
|
|
43
|
+
console.error(` zele login imap \\`)
|
|
44
|
+
console.error(` --email you@gmail.com \\`)
|
|
45
|
+
console.error(` --imap-host imap.gmail.com --imap-port 993 \\`)
|
|
46
|
+
console.error(` --smtp-host smtp.gmail.com --smtp-port 465 \\`)
|
|
47
|
+
console.error(` --password "your-app-password"`)
|
|
48
|
+
console.error()
|
|
49
|
+
console.error(pc.dim(' # Outlook/Hotmail'))
|
|
50
|
+
console.error(` zele login imap \\`)
|
|
51
|
+
console.error(` --email you@outlook.com \\`)
|
|
52
|
+
console.error(` --imap-host outlook.office365.com --imap-port 993 \\`)
|
|
53
|
+
console.error(` --smtp-host smtp-mail.outlook.com --smtp-port 587 \\`)
|
|
54
|
+
console.error(` --password "your-password"`)
|
|
55
|
+
console.error()
|
|
56
|
+
console.error(pc.dim(' # Generic (any IMAP/SMTP provider)'))
|
|
57
|
+
console.error(` zele login imap \\`)
|
|
58
|
+
console.error(` --email you@example.com \\`)
|
|
59
|
+
console.error(` --imap-host imap.example.com --imap-port 993 \\`)
|
|
60
|
+
console.error(` --smtp-host smtp.example.com --smtp-port 465 \\`)
|
|
61
|
+
console.error(` --password "your-password"`)
|
|
62
|
+
console.error()
|
|
63
|
+
console.error(pc.dim('Omit --smtp-host for read-only (IMAP only, no sending).'))
|
|
64
|
+
console.error(pc.dim('Use --imap-user/--smtp-user if the login username differs from your email.'))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Default: Google OAuth flow
|
|
15
70
|
const result = await login()
|
|
16
71
|
if (result instanceof Error) handleCommandError(result)
|
|
17
72
|
const { email } = result
|
|
@@ -20,6 +75,56 @@ export function registerAuthCommands(cli: Goke) {
|
|
|
20
75
|
process.exit(0)
|
|
21
76
|
})
|
|
22
77
|
|
|
78
|
+
cli
|
|
79
|
+
.command('login imap', 'Add an IMAP/SMTP email account (non-interactive, designed for agents)')
|
|
80
|
+
.option('--email <email>', z.string().describe('Email address'))
|
|
81
|
+
.option('--imap-host <imapHost>', z.string().describe('IMAP server hostname'))
|
|
82
|
+
.option('--imap-port <imapPort>', z.string().describe('IMAP server port (default: 993)'))
|
|
83
|
+
.option('--smtp-host <smtpHost>', z.string().describe('SMTP server hostname (optional, enables sending)'))
|
|
84
|
+
.option('--smtp-port <smtpPort>', z.string().describe('SMTP server port (default: 465)'))
|
|
85
|
+
.option('--password <password>', z.string().describe('Password (shared for IMAP and SMTP unless overridden)'))
|
|
86
|
+
.option('--imap-user <imapUser>', z.string().describe('IMAP username (defaults to --email)'))
|
|
87
|
+
.option('--imap-password <imapPassword>', z.string().describe('IMAP password (overrides --password)'))
|
|
88
|
+
.option('--smtp-user <smtpUser>', z.string().describe('SMTP username (defaults to --email)'))
|
|
89
|
+
.option('--smtp-password <smtpPassword>', z.string().describe('SMTP password (overrides --password)'))
|
|
90
|
+
.option('--no-tls', 'Disable TLS (not recommended)')
|
|
91
|
+
.action(async (options) => {
|
|
92
|
+
if (!options.email) {
|
|
93
|
+
out.error('--email is required')
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
if (!options.imapHost) {
|
|
97
|
+
out.error('--imap-host is required')
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
if (!options.password && !options.imapPassword) {
|
|
101
|
+
out.error('--password or --imap-password is required')
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
out.hint('Testing IMAP connection...')
|
|
106
|
+
|
|
107
|
+
const result = await loginImap({
|
|
108
|
+
email: options.email,
|
|
109
|
+
imapHost: options.imapHost,
|
|
110
|
+
imapPort: options.imapPort ? Number(options.imapPort) : undefined,
|
|
111
|
+
smtpHost: options.smtpHost,
|
|
112
|
+
smtpPort: options.smtpPort ? Number(options.smtpPort) : undefined,
|
|
113
|
+
password: options.password,
|
|
114
|
+
imapUser: options.imapUser,
|
|
115
|
+
imapPassword: options.imapPassword,
|
|
116
|
+
smtpUser: options.smtpUser,
|
|
117
|
+
smtpPassword: options.smtpPassword,
|
|
118
|
+
tls: options.noTls !== true,
|
|
119
|
+
})
|
|
120
|
+
if (result instanceof Error) handleCommandError(result)
|
|
121
|
+
|
|
122
|
+
const caps = options.smtpHost ? 'IMAP + SMTP' : 'IMAP only'
|
|
123
|
+
out.success(`Authenticated ${result.email} (${caps})`)
|
|
124
|
+
await closePrisma()
|
|
125
|
+
process.exit(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
23
128
|
cli
|
|
24
129
|
.command('logout [email]', 'Remove stored credentials for an account')
|
|
25
130
|
.option('--force', 'Skip confirmation')
|
|
@@ -88,9 +193,10 @@ export function registerAuthCommands(cli: Goke) {
|
|
|
88
193
|
out.printList(
|
|
89
194
|
statuses.map((s) => ({
|
|
90
195
|
email: s.email,
|
|
91
|
-
|
|
196
|
+
type: s.accountType,
|
|
197
|
+
capabilities: s.capabilities.join(', '),
|
|
92
198
|
status: 'Authenticated',
|
|
93
|
-
expires: s.expiresAt
|
|
199
|
+
...(s.expiresAt ? { expires: s.expiresAt.toISOString() } : {}),
|
|
94
200
|
})),
|
|
95
201
|
{ summary: `${statuses.length} account(s)` },
|
|
96
202
|
)
|
package/src/commands/draft.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
|
|
8
8
|
import fs from 'node:fs'
|
|
9
9
|
import { getClients, getClient } from '../auth.js'
|
|
10
10
|
import type { GmailClient } from '../gmail-client.js'
|
|
11
|
+
import type { ImapSmtpClient } from '../imap-smtp-client.js'
|
|
11
12
|
import { AuthError } from '../api-utils.js'
|
|
12
13
|
import * as out from '../output.js'
|
|
13
14
|
import { handleCommandError } from '../output.js'
|
|
@@ -154,6 +155,7 @@ export function registerDraftCommands(cli: Goke) {
|
|
|
154
155
|
threadId: options.thread,
|
|
155
156
|
fromEmail: options.from,
|
|
156
157
|
})
|
|
158
|
+
if (result instanceof Error) handleCommandError(result)
|
|
157
159
|
|
|
158
160
|
out.printYaml(result)
|
|
159
161
|
out.success('Draft created')
|
|
@@ -168,6 +170,7 @@ export function registerDraftCommands(cli: Goke) {
|
|
|
168
170
|
.action(async (draftId, options) => {
|
|
169
171
|
const { client } = await getClient(options.account)
|
|
170
172
|
const result = await client.sendDraft({ draftId })
|
|
173
|
+
if (result instanceof Error) handleCommandError(result)
|
|
171
174
|
|
|
172
175
|
out.printYaml(result)
|
|
173
176
|
out.success('Draft sent')
|
|
@@ -197,7 +200,8 @@ export function registerDraftCommands(cli: Goke) {
|
|
|
197
200
|
|
|
198
201
|
const { client } = await getClient(options.account)
|
|
199
202
|
|
|
200
|
-
await client.deleteDraft({ draftId })
|
|
203
|
+
const deleteResult = await client.deleteDraft({ draftId })
|
|
204
|
+
if (deleteResult instanceof Error) handleCommandError(deleteResult)
|
|
201
205
|
|
|
202
206
|
out.printYaml({ draft_id: draftId, deleted: true })
|
|
203
207
|
})
|
package/src/commands/filter.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Goke } from 'goke'
|
|
5
5
|
import { getClients } from '../auth.js'
|
|
6
|
-
import { AuthError, isScopeError } from '../api-utils.js'
|
|
6
|
+
import { AuthError, UnsupportedError, isScopeError } from '../api-utils.js'
|
|
7
|
+
import type { GmailClient } from '../gmail-client.js'
|
|
7
8
|
import * as out from '../output.js'
|
|
9
|
+
import { handleCommandError } from '../output.js'
|
|
8
10
|
|
|
9
11
|
export function registerFilterCommands(cli: Goke) {
|
|
10
12
|
// =========================================================================
|
|
@@ -15,10 +17,14 @@ export function registerFilterCommands(cli: Goke) {
|
|
|
15
17
|
.command('mail filter list', 'List all Gmail filters')
|
|
16
18
|
.action(async (options) => {
|
|
17
19
|
const clients = await getClients(options.account)
|
|
20
|
+
const googleClients = clients.filter((c) => c.accountType === 'google')
|
|
21
|
+
if (googleClients.length === 0) {
|
|
22
|
+
handleCommandError(new UnsupportedError({ feature: 'Filters', accountType: 'IMAP/SMTP', hint: 'Filters are a Gmail-specific feature.' }))
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
const results = await Promise.all(
|
|
20
|
-
|
|
21
|
-
const res = await client.listFilters()
|
|
26
|
+
googleClients.map(async ({ email, client }) => {
|
|
27
|
+
const res = await (client as GmailClient).listFilters()
|
|
22
28
|
if (res instanceof Error) return res
|
|
23
29
|
return { email, filters: res.parsed }
|
|
24
30
|
}),
|