Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/web/app/(app)/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { Trash2, ArrowRight } from "lucide-react";
import { Trash2, ArrowRight, BotIcon } from "lucide-react";
Comment thread
elie222 marked this conversation as resolved.
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { LoadingContent } from "@/components/LoadingContent";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -90,8 +90,13 @@ function AccountItem({
</div>
</CardHeader>
<CardContent className="flex justify-end gap-2">
<Button variant="outline" size="sm" Icon={ArrowRight} asChild>
<Link href={prefixPath(emailAccount.id, "/setup")}>View</Link>
<Button variant="outline" size="sm" Icon={BotIcon}>
<Link href={prefixPath(emailAccount.id, "/automation")}>
Assistant
</Link>
</Button>
<Button variant="outline" size="sm" Icon={ArrowRight}>
<Link href={prefixPath(emailAccount.id, "/setup")}>Setup</Link>
</Button>
Comment thread
elie222 marked this conversation as resolved.
{!emailAccount.isPrimary && (
<ConfirmDialog
Expand Down
30 changes: 9 additions & 21 deletions apps/web/app/api/google/watch/controller.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import type { gmail_v1 } from "@googleapis/gmail";
import prisma from "@/utils/prisma";
import { getGmailClientWithRefresh } from "@/utils/gmail/client";
import { captureException } from "@/utils/error";
import { createScopedLogger } from "@/utils/logger";
import { watchGmail, unwatchGmail } from "@/utils/gmail/watch";
import type { EmailProvider } from "@/utils/email/types";

const logger = createScopedLogger("google/watch");

export async function watchEmails({
emailAccountId,
gmail,
emailProvider,
}: {
emailAccountId: string;
gmail: gmail_v1.Gmail;
emailProvider: EmailProvider;
}) {
logger.info("Watching emails", { emailAccountId });
const res = await watchGmail(gmail);
const res = await emailProvider.watchEmails();

if (res.expiration) {
const expirationDate = new Date(+res.expiration);
if (res?.expirationDate) {
const expirationDate = new Date(res.expirationDate);
await prisma.emailAccount.update({
where: { id: emailAccountId },
data: { watchEmailsExpirationDate: expirationDate },
Expand All @@ -30,24 +28,14 @@ export async function watchEmails({

export async function unwatchEmails({
emailAccountId,
accessToken,
refreshToken,
expiresAt,
emailProvider,
}: {
emailAccountId: string;
accessToken: string | null;
refreshToken: string | null;
expiresAt: number | null;
emailProvider: EmailProvider;
}) {
try {
logger.info("Unwatching emails", { emailAccountId });
const gmail = await getGmailClientWithRefresh({
accessToken,
refreshToken,
expiresAt,
emailAccountId,
});
await unwatchGmail(gmail);
await emailProvider.unwatchEmails();
} catch (error) {
if (error instanceof Error && error.message.includes("invalid_grant")) {
logger.warn("Error unwatching emails, invalid grant", { emailAccountId });
Expand Down
12 changes: 9 additions & 3 deletions apps/web/app/api/google/watch/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
import { watchEmails } from "./controller";
import { withAuth } from "@/utils/middleware";
import { createScopedLogger } from "@/utils/logger";
import { getGmailClientForEmailId } from "@/utils/account";
import prisma from "@/utils/prisma";
import { createEmailProvider } from "@/utils/email/provider";

export const dynamic = "force-dynamic";

Expand All @@ -27,8 +27,14 @@ export const GET = withAuth(async (request) => {

for (const { id: emailAccountId } of emailAccounts) {
try {
const gmail = await getGmailClientForEmailId({ emailAccountId });
const expirationDate = await watchEmails({ emailAccountId, gmail });
const emailProvider = await createEmailProvider({
emailAccountId,
provider: "google",
Comment thread
elie222 marked this conversation as resolved.
});
const expirationDate = await watchEmails({
emailAccountId,
emailProvider,
});

if (expirationDate) {
results.push({
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/api/threads/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const threadsQuery = z.object({
type: z.string().nullish(),
nextPageToken: z.string().nullish(),
labelId: z.string().nullish(), // For Google
labelIds: z.array(z.string()).nullish(), // For Google
excludeLabelNames: z.array(z.string()).nullish(), // For Google
after: z.coerce.date().nullish(),
before: z.coerce.date().nullish(),
isUnread: z.coerce.boolean().nullish(),
Expand Down
40 changes: 21 additions & 19 deletions apps/web/app/api/user/group/[groupId]/messages/controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import prisma from "@/utils/prisma";
import type { gmail_v1 } from "@googleapis/gmail";
import { createHash } from "node:crypto";
import groupBy from "lodash/groupBy";
import { getMessage, getMessages } from "@/utils/gmail/message";
import { findMatchingGroupItem } from "@/utils/group/find-matching-group";
import { parseMessage } from "@/utils/gmail/message";
import { extractEmailAddress } from "@/utils/email";
import { type GroupItem, GroupItemType } from "@prisma/client";
import type { MessageWithGroupItem } from "@/app/(app)/[emailAccountId]/assistant/rule/[ruleId]/examples/types";
import { SafeError } from "@/utils/error";
import { createEmailProvider } from "@/utils/email/provider";
import type { EmailProvider } from "@/utils/email/types";

const PAGE_SIZE = 20;

Expand All @@ -22,16 +21,16 @@ interface InternalPaginationState {
export type GroupEmailsResponse = Awaited<ReturnType<typeof getGroupEmails>>;

export async function getGroupEmails({
provider,
groupId,
emailAccountId,
gmail,
from,
to,
pageToken,
}: {
provider: string;
groupId: string;
emailAccountId: string;
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
Expand All @@ -43,9 +42,14 @@ export async function getGroupEmails({

if (!group) throw new SafeError("Group not found");

const emailProvider = await createEmailProvider({
emailAccountId,
provider,
});

const { messages, nextPageToken } = await fetchPaginatedMessages({
emailProvider,
groupItems: group.items,
gmail,
from,
to,
pageToken,
Expand All @@ -55,14 +59,14 @@ export async function getGroupEmails({
}

export async function fetchPaginatedMessages({
emailProvider,
groupItems,
gmail,
from,
to,
pageToken,
}: {
emailProvider: EmailProvider;
groupItems: GroupItem[];
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
Expand Down Expand Up @@ -97,7 +101,7 @@ export async function fetchPaginatedMessages({

const { messages, nextPaginationState } = await fetchPaginatedGroupMessages(
groupItems,
gmail,
emailProvider,
from,
to,
paginationState,
Expand Down Expand Up @@ -126,7 +130,7 @@ function createGroupItemsHash(
// and for each type, through multiple chunks
async function fetchPaginatedGroupMessages(
groupItems: GroupItem[],
gmail: gmail_v1.Gmail,
emailProvider: EmailProvider,
from: Date | undefined,
to: Date | undefined,
paginationState: InternalPaginationState,
Expand Down Expand Up @@ -157,7 +161,7 @@ async function fetchPaginatedGroupMessages(
const result = await fetchGroupMessages(
type,
chunk,
gmail,
emailProvider,
PAGE_SIZE - messages.length,
from,
to,
Expand Down Expand Up @@ -206,30 +210,28 @@ async function fetchPaginatedGroupMessages(
async function fetchGroupMessages(
groupItemType: GroupItemType,
groupItems: GroupItem[],
gmail: gmail_v1.Gmail,
emailProvider: EmailProvider,
maxResults: number,
from?: Date,
to?: Date,
pageToken?: string,
): Promise<{ messages: MessageWithGroupItem[]; nextPageToken?: string }> {
const query = buildQuery(groupItemType, groupItems, from, to);

const response = await getMessages(gmail, {
const response = await emailProvider.getMessagesWithPagination({
query,
maxResults,
pageToken,
});

const messages = await Promise.all(
(response.messages || []).map(async (message) => {
// TODO: Use email provider to get the message which will parse it internally
const m = await getMessage(message.id!, gmail);
const parsedMessage = parseMessage(m);
(response.messages || []).map(async (m) => {
const message = await emailProvider.getMessage(m.id);
const matchingGroupItem = findMatchingGroupItem(
parsedMessage.headers,
message.headers,
groupItems,
);
return { ...parsedMessage, matchingGroupItem };
return { ...message, matchingGroupItem };
}),
Comment thread
elie222 marked this conversation as resolved.
);

Expand Down
9 changes: 3 additions & 6 deletions apps/web/app/api/user/group/[groupId]/messages/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { NextResponse } from "next/server";
import { withEmailAccount } from "@/utils/middleware";
import { withEmailProvider } from "@/utils/middleware";
import { getGroupEmails } from "@/app/api/user/group/[groupId]/messages/controller";
import { getGmailClientForEmail } from "@/utils/account";

export const GET = withEmailAccount(async (request, { params }) => {
export const GET = withEmailProvider(async (request, { params }) => {
const emailAccountId = request.auth.emailAccountId;

const { groupId } = await params;
if (!groupId) return NextResponse.json({ error: "Missing group id" });

const gmail = await getGmailClientForEmail({ emailAccountId });

const { messages } = await getGroupEmails({
provider: request.emailProvider.name,
groupId,
emailAccountId,
gmail,
from: undefined,
to: undefined,
pageToken: "",
Expand Down
51 changes: 23 additions & 28 deletions apps/web/app/api/user/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { NextResponse } from "next/server";
import { dateToSeconds } from "@/utils/date";
import { getMessages } from "@/utils/gmail/message";
import { getGmailClientForEmailId } from "@/utils/account";
import { withEmailAccount } from "@/utils/middleware";
import { withEmailProvider } from "@/utils/middleware";
import type { EmailProvider } from "@/utils/email/types";

export type StatsResponse = Awaited<ReturnType<typeof getStats>>;

async function getStats(options: { gmail: gmail_v1.Gmail }) {
const { gmail } = options;

async function getStats({ emailProvider }: { emailProvider: EmailProvider }) {
const now = new Date();
const twentyFourHoursAgo = new Date(
now.getFullYear(),
Expand All @@ -22,9 +17,6 @@ async function getStats(options: { gmail: gmail_v1.Gmail }) {
now.getDate() - 7,
);

const twentyFourHoursAgoInSeconds = dateToSeconds(twentyFourHoursAgo);
const sevenDaysAgoInSeconds = dateToSeconds(sevenDaysAgo);

const [
emailsReceived24hrs,
emailsSent24hrs,
Expand All @@ -33,30 +25,36 @@ async function getStats(options: { gmail: gmail_v1.Gmail }) {
emailsSent7days,
emailsInbox7days,
] = await Promise.all([
getMessages(gmail, {
query: `-in:sent after:${twentyFourHoursAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: twentyFourHoursAgo,
excludeSent: true,
maxResults: 500,
}),
getMessages(gmail, {
query: `in:sent after:${twentyFourHoursAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: twentyFourHoursAgo,
type: "sent",
maxResults: 500,
}),
getMessages(gmail, {
query: `in:inbox after:${twentyFourHoursAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: twentyFourHoursAgo,
type: "inbox",
maxResults: 500,
}),

// 7 days
getMessages(gmail, {
query: `-in:sent after:${sevenDaysAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: sevenDaysAgo,
excludeSent: true,
maxResults: 500,
}),
getMessages(gmail, {
query: `in:sent after:${sevenDaysAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: sevenDaysAgo,
type: "sent",
maxResults: 500,
}),
getMessages(gmail, {
query: `in:inbox after:${sevenDaysAgoInSeconds}`,
emailProvider.getMessagesByFields({
after: sevenDaysAgo,
type: "inbox",
maxResults: 500,
}),
]);
Expand All @@ -72,11 +70,8 @@ async function getStats(options: { gmail: gmail_v1.Gmail }) {
};
}

export const GET = withEmailAccount(async (request) => {
const emailAccountId = request.auth.emailAccountId;

const gmail = await getGmailClientForEmailId({ emailAccountId });
const result = await getStats({ gmail });
export const GET = withEmailProvider(async (request) => {
const result = await getStats({ emailProvider: request.emailProvider });

return NextResponse.json(result);
});
10 changes: 5 additions & 5 deletions apps/web/components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ export const useNavigation = () => {
href: prefixPath(currentEmailAccountId, "/clean"),
icon: BrushIcon,
},
{
name: "Analytics",
href: prefixPath(currentEmailAccountId, "/stats"),
icon: BarChartBigIcon,
},
]
: []),
{
name: "Analytics",
href: prefixPath(currentEmailAccountId, "/stats"),
icon: BarChartBigIcon,
},
],
[currentEmailAccountId, provider],
);
Expand Down
Loading
Loading