veryfront 0.0.45 → 0.0.46
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/dist/ai/index.d.ts +11 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/index.js.map +2 -2
- package/dist/cli.js +6 -4
- package/dist/components.js +1 -1
- package/dist/components.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/data.js +1 -1
- package/dist/data.js.map +1 -1
- package/dist/index.js +32 -33
- package/dist/index.js.map +2 -2
- package/dist/integrations/_base/connector.json +11 -0
- package/dist/integrations/_base/files/SETUP.md +132 -0
- package/dist/integrations/_base/files/app/api/integrations/status/route.ts +38 -0
- package/dist/integrations/_base/files/app/setup/page.tsx +461 -0
- package/dist/integrations/_base/files/lib/oauth.ts +145 -0
- package/dist/integrations/_base/files/lib/token-store.ts +109 -0
- package/dist/integrations/calendar/connector.json +77 -0
- package/dist/integrations/calendar/files/ai/tools/create-event.ts +83 -0
- package/dist/integrations/calendar/files/ai/tools/find-free-time.ts +108 -0
- package/dist/integrations/calendar/files/ai/tools/list-events.ts +98 -0
- package/dist/integrations/calendar/files/app/api/auth/calendar/callback/route.ts +114 -0
- package/dist/integrations/calendar/files/app/api/auth/calendar/route.ts +29 -0
- package/dist/integrations/calendar/files/lib/calendar-client.ts +309 -0
- package/dist/integrations/calendar/files/lib/oauth.ts +145 -0
- package/dist/integrations/calendar/files/lib/token-store.ts +109 -0
- package/dist/integrations/github/connector.json +84 -0
- package/dist/integrations/github/files/ai/tools/create-issue.ts +75 -0
- package/dist/integrations/github/files/ai/tools/get-pr-diff.ts +82 -0
- package/dist/integrations/github/files/ai/tools/list-prs.ts +93 -0
- package/dist/integrations/github/files/ai/tools/list-repos.ts +81 -0
- package/dist/integrations/github/files/app/api/auth/github/callback/route.ts +132 -0
- package/dist/integrations/github/files/app/api/auth/github/route.ts +29 -0
- package/dist/integrations/github/files/lib/github-client.ts +282 -0
- package/dist/integrations/github/files/lib/oauth.ts +145 -0
- package/dist/integrations/github/files/lib/token-store.ts +109 -0
- package/dist/integrations/gmail/connector.json +78 -0
- package/dist/integrations/gmail/files/ai/tools/list-emails.ts +92 -0
- package/dist/integrations/gmail/files/ai/tools/search-emails.ts +92 -0
- package/dist/integrations/gmail/files/ai/tools/send-email.ts +77 -0
- package/dist/integrations/gmail/files/app/api/auth/gmail/callback/route.ts +114 -0
- package/dist/integrations/gmail/files/app/api/auth/gmail/route.ts +29 -0
- package/dist/integrations/gmail/files/lib/gmail-client.ts +259 -0
- package/dist/integrations/gmail/files/lib/oauth.ts +145 -0
- package/dist/integrations/gmail/files/lib/token-store.ts +109 -0
- package/dist/integrations/slack/connector.json +74 -0
- package/dist/integrations/slack/files/ai/tools/get-messages.ts +65 -0
- package/dist/integrations/slack/files/ai/tools/list-channels.ts +63 -0
- package/dist/integrations/slack/files/ai/tools/send-message.ts +49 -0
- package/dist/integrations/slack/files/app/api/auth/slack/callback/route.ts +132 -0
- package/dist/integrations/slack/files/app/api/auth/slack/route.ts +29 -0
- package/dist/integrations/slack/files/lib/oauth.ts +145 -0
- package/dist/integrations/slack/files/lib/slack-client.ts +215 -0
- package/dist/integrations/slack/files/lib/token-store.ts +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGmailClient, parseEmailHeaders } from "../../lib/gmail-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "search-emails",
|
|
7
|
+
description:
|
|
8
|
+
"Search emails using Gmail's search syntax. Supports queries like 'from:person@email.com', 'subject:meeting', 'after:2024/01/01', etc.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
query: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe(
|
|
14
|
+
"Search query using Gmail search syntax (e.g., 'from:boss@company.com subject:urgent')",
|
|
15
|
+
),
|
|
16
|
+
maxResults: z
|
|
17
|
+
.number()
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(50)
|
|
20
|
+
.default(10)
|
|
21
|
+
.describe("Maximum number of results to return"),
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ query, maxResults }, context) => {
|
|
24
|
+
const userId = context?.userId as string | undefined;
|
|
25
|
+
if (!userId) {
|
|
26
|
+
return {
|
|
27
|
+
error: "User not authenticated. Please log in first.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const gmail = createGmailClient(userId);
|
|
33
|
+
|
|
34
|
+
const list = await gmail.listMessages({
|
|
35
|
+
query,
|
|
36
|
+
maxResults,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!list.messages || list.messages.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
emails: [],
|
|
42
|
+
query,
|
|
43
|
+
message: `No emails found matching: "${query}"`,
|
|
44
|
+
searchTips: [
|
|
45
|
+
"from:email@example.com - Search by sender",
|
|
46
|
+
"to:email@example.com - Search by recipient",
|
|
47
|
+
"subject:keywords - Search in subject",
|
|
48
|
+
"after:YYYY/MM/DD - Emails after date",
|
|
49
|
+
"before:YYYY/MM/DD - Emails before date",
|
|
50
|
+
"is:unread - Unread emails only",
|
|
51
|
+
"has:attachment - Emails with attachments",
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fetch metadata for each email
|
|
57
|
+
const emails = await Promise.all(
|
|
58
|
+
list.messages.map(async (m: { id: string }) => {
|
|
59
|
+
const message = await gmail.getMessage(m.id, "metadata");
|
|
60
|
+
const headers = parseEmailHeaders(message.payload?.headers || []);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: message.id,
|
|
64
|
+
threadId: message.threadId,
|
|
65
|
+
from: headers.from,
|
|
66
|
+
to: headers.to,
|
|
67
|
+
subject: headers.subject,
|
|
68
|
+
date: headers.date,
|
|
69
|
+
snippet: message.snippet,
|
|
70
|
+
isUnread: message.labelIds?.includes("UNREAD") || false,
|
|
71
|
+
labels: message.labelIds,
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
emails,
|
|
78
|
+
query,
|
|
79
|
+
count: emails.length,
|
|
80
|
+
message: `Found ${emails.length} email(s) matching: "${query}"`,
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
84
|
+
return {
|
|
85
|
+
error: "Gmail not connected. Please connect your Gmail account.",
|
|
86
|
+
connectUrl: "/api/auth/gmail",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGmailClient } from "../../lib/gmail-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "send-email",
|
|
7
|
+
description: "Send an email via Gmail. Can send to multiple recipients with CC and BCC support.",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
to: z
|
|
10
|
+
.union([z.string().email(), z.array(z.string().email())])
|
|
11
|
+
.describe("Email recipient(s)"),
|
|
12
|
+
subject: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Email subject line"),
|
|
16
|
+
body: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe("Email body content"),
|
|
20
|
+
cc: z
|
|
21
|
+
.union([z.string().email(), z.array(z.string().email())])
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("CC recipient(s)"),
|
|
24
|
+
bcc: z
|
|
25
|
+
.union([z.string().email(), z.array(z.string().email())])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("BCC recipient(s)"),
|
|
28
|
+
isHtml: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.default(false)
|
|
31
|
+
.describe("Whether the body contains HTML"),
|
|
32
|
+
}),
|
|
33
|
+
execute: async ({ to, subject, body, cc, bcc, isHtml }, context) => {
|
|
34
|
+
const userId = context?.userId as string | undefined;
|
|
35
|
+
if (!userId) {
|
|
36
|
+
return {
|
|
37
|
+
error: "User not authenticated. Please log in first.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const gmail = createGmailClient(userId);
|
|
43
|
+
|
|
44
|
+
const result = await gmail.sendEmail({
|
|
45
|
+
to,
|
|
46
|
+
subject,
|
|
47
|
+
body,
|
|
48
|
+
cc,
|
|
49
|
+
bcc,
|
|
50
|
+
isHtml,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const recipients = Array.isArray(to) ? to.join(", ") : to;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
messageId: result.id,
|
|
58
|
+
threadId: result.threadId,
|
|
59
|
+
message: `Email sent successfully to ${recipients}.`,
|
|
60
|
+
details: {
|
|
61
|
+
to: recipients,
|
|
62
|
+
subject,
|
|
63
|
+
cc: cc ? (Array.isArray(cc) ? cc.join(", ") : cc) : undefined,
|
|
64
|
+
bcc: bcc ? (Array.isArray(bcc) ? bcc.join(", ") : bcc) : undefined,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
69
|
+
return {
|
|
70
|
+
error: "Gmail not connected. Please connect your Gmail account.",
|
|
71
|
+
connectUrl: "/api/auth/gmail",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail OAuth Callback
|
|
3
|
+
*
|
|
4
|
+
* Handles the OAuth callback from Google, exchanges code for tokens,
|
|
5
|
+
* and stores them securely.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exchangeCodeForTokens } from "../../../../../lib/oauth.ts";
|
|
9
|
+
import { tokenStore } from "../../../../../lib/token-store.ts";
|
|
10
|
+
import { gmailOAuthProvider } from "../../../../../lib/gmail-client.ts";
|
|
11
|
+
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const code = url.searchParams.get("code");
|
|
15
|
+
const state = url.searchParams.get("state");
|
|
16
|
+
const error = url.searchParams.get("error");
|
|
17
|
+
|
|
18
|
+
// Handle OAuth errors
|
|
19
|
+
if (error) {
|
|
20
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
21
|
+
return new Response(
|
|
22
|
+
`
|
|
23
|
+
<html>
|
|
24
|
+
<head><title>Connection Failed</title></head>
|
|
25
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
26
|
+
<h1>Gmail Connection Failed</h1>
|
|
27
|
+
<p style="color: #666;">${errorDescription}</p>
|
|
28
|
+
<a href="/" style="color: #0066cc;">Return to App</a>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
`,
|
|
32
|
+
{
|
|
33
|
+
status: 400,
|
|
34
|
+
headers: { "Content-Type": "text/html" },
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!code || !state) {
|
|
40
|
+
return new Response("Missing code or state parameter", { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate state from cookie
|
|
44
|
+
const cookies = req.headers.get("cookie") || "";
|
|
45
|
+
const stateCookie = cookies
|
|
46
|
+
.split(";")
|
|
47
|
+
.find((c) => c.trim().startsWith("gmail_oauth_state="));
|
|
48
|
+
const savedState = stateCookie?.split("=")[1]?.trim();
|
|
49
|
+
|
|
50
|
+
if (state !== savedState) {
|
|
51
|
+
return new Response("Invalid state parameter", { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const origin = url.origin;
|
|
56
|
+
const redirectUri = `${origin}${gmailOAuthProvider.callbackPath}`;
|
|
57
|
+
|
|
58
|
+
// Exchange code for tokens
|
|
59
|
+
const tokens = await exchangeCodeForTokens(
|
|
60
|
+
gmailOAuthProvider,
|
|
61
|
+
code,
|
|
62
|
+
redirectUri,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Get actual user ID from session in production
|
|
66
|
+
const userId = "current-user";
|
|
67
|
+
|
|
68
|
+
// Store tokens
|
|
69
|
+
await tokenStore.setToken(userId, "gmail", tokens);
|
|
70
|
+
|
|
71
|
+
// Clear state cookie and redirect to success page
|
|
72
|
+
return new Response(
|
|
73
|
+
`
|
|
74
|
+
<html>
|
|
75
|
+
<head>
|
|
76
|
+
<title>Gmail Connected</title>
|
|
77
|
+
<meta http-equiv="refresh" content="2;url=/" />
|
|
78
|
+
</head>
|
|
79
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
80
|
+
<h1 style="color: #22c55e;">✓ Gmail Connected!</h1>
|
|
81
|
+
<p style="color: #666;">Your Gmail account has been connected successfully.</p>
|
|
82
|
+
<p style="color: #999; font-size: 14px;">Redirecting...</p>
|
|
83
|
+
<a href="/" style="color: #0066cc;">Return to App</a>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
86
|
+
`,
|
|
87
|
+
{
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "text/html",
|
|
91
|
+
"Set-Cookie": "gmail_oauth_state=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("Gmail OAuth error:", err);
|
|
97
|
+
return new Response(
|
|
98
|
+
`
|
|
99
|
+
<html>
|
|
100
|
+
<head><title>Connection Failed</title></head>
|
|
101
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
102
|
+
<h1>Gmail Connection Failed</h1>
|
|
103
|
+
<p style="color: #666;">Unable to complete Gmail authorization. Please try again.</p>
|
|
104
|
+
<a href="/api/auth/gmail" style="color: #0066cc;">Try Again</a>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
`,
|
|
108
|
+
{
|
|
109
|
+
status: 500,
|
|
110
|
+
headers: { "Content-Type": "text/html" },
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail OAuth Initiation
|
|
3
|
+
*
|
|
4
|
+
* Redirects to Google OAuth consent screen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAuthorizationUrl } from "../../../../lib/oauth.ts";
|
|
8
|
+
import { gmailOAuthProvider } from "../../../../lib/gmail-client.ts";
|
|
9
|
+
|
|
10
|
+
export function GET(req: Request) {
|
|
11
|
+
const url = new URL(req.url);
|
|
12
|
+
const origin = url.origin;
|
|
13
|
+
|
|
14
|
+
// Generate a random state for CSRF protection
|
|
15
|
+
const state = crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
// Store state in a cookie for validation
|
|
18
|
+
const redirectUri = `${origin}${gmailOAuthProvider.callbackPath}`;
|
|
19
|
+
const authUrl = getAuthorizationUrl(gmailOAuthProvider, state, redirectUri);
|
|
20
|
+
|
|
21
|
+
// Set state cookie and redirect to Google
|
|
22
|
+
return new Response(null, {
|
|
23
|
+
status: 302,
|
|
24
|
+
headers: {
|
|
25
|
+
Location: authUrl,
|
|
26
|
+
"Set-Cookie": `gmail_oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail API Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a type-safe interface to Gmail API operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { tokenStore as _tokenStore } from "./token-store.ts";
|
|
8
|
+
import { getValidToken } from "./oauth.ts";
|
|
9
|
+
|
|
10
|
+
// Helper for Cross-Platform environment access
|
|
11
|
+
function getEnv(key: string): string | undefined {
|
|
12
|
+
// @ts-ignore - Deno global
|
|
13
|
+
if (typeof Deno !== "undefined") {
|
|
14
|
+
// @ts-ignore - Deno global
|
|
15
|
+
return Deno.env.get(key);
|
|
16
|
+
} // @ts-ignore - process global
|
|
17
|
+
else if (typeof process !== "undefined" && process.env) {
|
|
18
|
+
// @ts-ignore - process global
|
|
19
|
+
return process.env[key];
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1";
|
|
25
|
+
|
|
26
|
+
export interface GmailMessage {
|
|
27
|
+
id: string;
|
|
28
|
+
threadId: string;
|
|
29
|
+
labelIds: string[];
|
|
30
|
+
snippet: string;
|
|
31
|
+
payload?: {
|
|
32
|
+
headers: Array<{ name: string; value: string }>;
|
|
33
|
+
body?: { data?: string; size: number };
|
|
34
|
+
parts?: Array<{
|
|
35
|
+
mimeType: string;
|
|
36
|
+
body?: { data?: string; size: number };
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
internalDate: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GmailMessageList {
|
|
43
|
+
messages: Array<{ id: string; threadId: string }>;
|
|
44
|
+
nextPageToken?: string;
|
|
45
|
+
resultSizeEstimate: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SendEmailOptions {
|
|
49
|
+
to: string | string[];
|
|
50
|
+
subject: string;
|
|
51
|
+
body: string;
|
|
52
|
+
cc?: string | string[];
|
|
53
|
+
bcc?: string | string[];
|
|
54
|
+
replyTo?: string;
|
|
55
|
+
isHtml?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gmail OAuth provider configuration
|
|
60
|
+
*/
|
|
61
|
+
export const gmailOAuthProvider = {
|
|
62
|
+
name: "gmail",
|
|
63
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
64
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
65
|
+
clientId: getEnv("GOOGLE_CLIENT_ID") || "",
|
|
66
|
+
clientSecret: getEnv("GOOGLE_CLIENT_SECRET") || "",
|
|
67
|
+
scopes: [
|
|
68
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
69
|
+
"https://www.googleapis.com/auth/gmail.send",
|
|
70
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
71
|
+
],
|
|
72
|
+
callbackPath: "/api/auth/gmail/callback",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a Gmail client for a specific user
|
|
77
|
+
*/
|
|
78
|
+
export function createGmailClient(userId: string) {
|
|
79
|
+
async function getAccessToken(): Promise<string> {
|
|
80
|
+
const token = await getValidToken(gmailOAuthProvider, userId, "gmail");
|
|
81
|
+
if (!token) {
|
|
82
|
+
throw new Error("Gmail not connected. Please connect your Gmail account first.");
|
|
83
|
+
}
|
|
84
|
+
return token;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function apiRequest<T>(
|
|
88
|
+
endpoint: string,
|
|
89
|
+
options: RequestInit = {},
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
const accessToken = await getAccessToken();
|
|
92
|
+
|
|
93
|
+
const response = await fetch(`${GMAIL_API_BASE}${endpoint}`, {
|
|
94
|
+
...options,
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${accessToken}`,
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
...options.headers,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const error = await response.text();
|
|
104
|
+
throw new Error(`Gmail API error: ${response.status} - ${error}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return response.json();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
/**
|
|
112
|
+
* List messages from the user's mailbox
|
|
113
|
+
*/
|
|
114
|
+
listMessages(options: {
|
|
115
|
+
maxResults?: number;
|
|
116
|
+
query?: string;
|
|
117
|
+
labelIds?: string[];
|
|
118
|
+
pageToken?: string;
|
|
119
|
+
} = {}): Promise<GmailMessageList> {
|
|
120
|
+
const params = new URLSearchParams();
|
|
121
|
+
if (options.maxResults) params.set("maxResults", String(options.maxResults));
|
|
122
|
+
if (options.query) params.set("q", options.query);
|
|
123
|
+
if (options.labelIds) params.set("labelIds", options.labelIds.join(","));
|
|
124
|
+
if (options.pageToken) params.set("pageToken", options.pageToken);
|
|
125
|
+
|
|
126
|
+
const query = params.toString();
|
|
127
|
+
return apiRequest<GmailMessageList>(
|
|
128
|
+
`/users/me/messages${query ? `?${query}` : ""}`,
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get a specific message by ID
|
|
134
|
+
*/
|
|
135
|
+
getMessage(
|
|
136
|
+
messageId: string,
|
|
137
|
+
format: "full" | "metadata" | "minimal" = "full",
|
|
138
|
+
): Promise<GmailMessage> {
|
|
139
|
+
return apiRequest<GmailMessage>(
|
|
140
|
+
`/users/me/messages/${messageId}?format=${format}`,
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send an email
|
|
146
|
+
*/
|
|
147
|
+
sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }> {
|
|
148
|
+
const toAddresses = Array.isArray(options.to) ? options.to.join(", ") : options.to;
|
|
149
|
+
const ccAddresses = options.cc
|
|
150
|
+
? Array.isArray(options.cc) ? options.cc.join(", ") : options.cc
|
|
151
|
+
: "";
|
|
152
|
+
const bccAddresses = options.bcc
|
|
153
|
+
? Array.isArray(options.bcc) ? options.bcc.join(", ") : options.bcc
|
|
154
|
+
: "";
|
|
155
|
+
|
|
156
|
+
const headers = [
|
|
157
|
+
`To: ${toAddresses}`,
|
|
158
|
+
`Subject: ${options.subject}`,
|
|
159
|
+
options.isHtml
|
|
160
|
+
? "Content-Type: text/html; charset=utf-8"
|
|
161
|
+
: "Content-Type: text/plain; charset=utf-8",
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
if (ccAddresses) headers.push(`Cc: ${ccAddresses}`);
|
|
165
|
+
if (bccAddresses) headers.push(`Bcc: ${bccAddresses}`);
|
|
166
|
+
if (options.replyTo) headers.push(`Reply-To: ${options.replyTo}`);
|
|
167
|
+
|
|
168
|
+
const email = `${headers.join("\r\n")}\r\n\r\n${options.body}`;
|
|
169
|
+
|
|
170
|
+
// Encode email as base64url
|
|
171
|
+
const encodedEmail = btoa(email)
|
|
172
|
+
.replace(/\+/g, "-")
|
|
173
|
+
.replace(/\//g, "_")
|
|
174
|
+
.replace(/=+$/, "");
|
|
175
|
+
|
|
176
|
+
return apiRequest<{ id: string; threadId: string }>("/users/me/messages/send", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ raw: encodedEmail }),
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Search emails by query
|
|
184
|
+
*/
|
|
185
|
+
async searchEmails(query: string, maxResults = 10): Promise<GmailMessage[]> {
|
|
186
|
+
const list = await this.listMessages({ query, maxResults });
|
|
187
|
+
|
|
188
|
+
if (!list.messages || list.messages.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fetch full message details
|
|
193
|
+
const messages = await Promise.all(
|
|
194
|
+
list.messages.map((m) => this.getMessage(m.id, "metadata")),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return messages;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get unread emails
|
|
202
|
+
*/
|
|
203
|
+
getUnreadEmails(maxResults = 10): Promise<GmailMessage[]> {
|
|
204
|
+
return this.searchEmails("is:unread", maxResults);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mark email as read
|
|
209
|
+
*/
|
|
210
|
+
async markAsRead(messageId: string): Promise<void> {
|
|
211
|
+
await apiRequest(`/users/me/messages/${messageId}/modify`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
removeLabelIds: ["UNREAD"],
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Archive an email
|
|
221
|
+
*/
|
|
222
|
+
async archiveEmail(messageId: string): Promise<void> {
|
|
223
|
+
await apiRequest(`/users/me/messages/${messageId}/modify`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
removeLabelIds: ["INBOX"],
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse email headers to extract common fields
|
|
235
|
+
*/
|
|
236
|
+
export function parseEmailHeaders(
|
|
237
|
+
headers: Array<{ name: string; value: string }>,
|
|
238
|
+
): {
|
|
239
|
+
from: string;
|
|
240
|
+
to: string;
|
|
241
|
+
subject: string;
|
|
242
|
+
date: string;
|
|
243
|
+
} {
|
|
244
|
+
const getHeader = (name: string): string => {
|
|
245
|
+
const header = headers.find(
|
|
246
|
+
(h) => h.name.toLowerCase() === name.toLowerCase(),
|
|
247
|
+
);
|
|
248
|
+
return header?.value || "";
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
from: getHeader("From"),
|
|
253
|
+
to: getHeader("To"),
|
|
254
|
+
subject: getHeader("Subject"),
|
|
255
|
+
date: getHeader("Date"),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type GmailClient = ReturnType<typeof createGmailClient>;
|