Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
20 changes: 10 additions & 10 deletions apps/web/utils/actions/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { createEmailProvider } from "@/utils/email/provider";
import { isGoogleProvider } from "@/utils/email/provider-types";
import { getUserPremium } from "@/utils/user/get";
import { isActivePremium } from "@/utils/premium";
import { ONE_DAY_MS } from "@/utils/date";

export const cleanInboxAction = actionClient
.metadata({ name: "cleanInbox" })
Expand All @@ -48,7 +49,6 @@ export const cleanInboxAction = actionClient
if (!premium) throw new SafeError("User not premium");
if (!isActivePremium(premium)) throw new SafeError("Premium not active");

const gmail = await getGmailClientForEmail({ emailAccountId });
const emailProvider = await createEmailProvider({
emailAccountId,
provider,
Expand Down Expand Up @@ -113,18 +113,18 @@ export const cleanInboxAction = actionClient

let totalEmailsProcessed = 0;

const query = `${daysOld ? `older_than:${daysOld}d ` : ""}-in:"${inboxZeroLabels.processed.name}"`;

do {
// fetch all emails from the user's inbox
const { threads, nextPageToken: pageToken } =
await getThreadsWithNextPageToken({
gmail,
q: query,
labelIds:
type === "inbox"
? [GmailLabel.INBOX]
: [GmailLabel.INBOX, GmailLabel.UNREAD],
await emailProvider.getThreadsWithQuery({
Comment thread
elie222 marked this conversation as resolved.
query: {
before: new Date(Date.now() - daysOld * ONE_DAY_MS),
Comment thread
elie222 marked this conversation as resolved.
Outdated
labelIds:
type === "inbox"
? [GmailLabel.INBOX]
: [GmailLabel.INBOX, GmailLabel.UNREAD],
excludeLabelNames: [inboxZeroLabels.processed.name],
},
maxResults: Math.min(maxEmails || 100, 100),
});
Comment thread
elie222 marked this conversation as resolved.

Expand Down
11 changes: 6 additions & 5 deletions apps/web/utils/actions/whitelist.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
"use server";

import { env } from "@/env";
import { createFilter } from "@/utils/gmail/filter";
import { GmailLabel } from "@/utils/gmail/label";
import { actionClient } from "@/utils/actions/safe-action";
import { getGmailClientForEmail } from "@/utils/account";
import { isGoogleProvider } from "@/utils/email/provider-types";
import { createEmailProvider } from "@/utils/email/provider";

export const whitelistInboxZeroAction = actionClient
.metadata({ name: "whitelistInboxZero" })
.action(async ({ ctx: { emailAccountId, provider } }) => {
if (!env.WHITELIST_FROM) return;
if (!isGoogleProvider(provider)) return;

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

await createFilter({
gmail,
await emailProvider.createFilter({
from: env.WHITELIST_FROM,
addLabelIds: ["CATEGORY_PERSONAL", GmailLabel.IMPORTANT],
removeLabelIds: [GmailLabel.SPAM],
Expand Down
39 changes: 27 additions & 12 deletions apps/web/utils/email/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,37 +610,54 @@ export class GmailProvider implements EmailProvider {
threads: EmailThread[];
nextPageToken?: string;
}> {
const query = options.query;
const {
fromEmail,
after,
before,
isUnread,
type,
excludeLabelNames,
labelIds,
labelId,
} = options.query || {};

function getQuery() {
const queryParts: string[] = [];

if (query?.fromEmail) {
queryParts.push(`from:${query.fromEmail}`);
if (fromEmail) {
queryParts.push(`from:${fromEmail}`);
}

if (query?.after) {
const afterSeconds = Math.floor(query.after.getTime() / 1000);
if (after) {
const afterSeconds = Math.floor(after.getTime() / 1000);
queryParts.push(`after:${afterSeconds}`);
}

if (query?.before) {
const beforeSeconds = Math.floor(query.before.getTime() / 1000);
if (before) {
const beforeSeconds = Math.floor(before.getTime() / 1000);
queryParts.push(`before:${beforeSeconds}`);
}

if (query?.isUnread) {
if (isUnread) {
queryParts.push("is:unread");
}

if (query?.type === "archive") {
if (type === "archive") {
queryParts.push(`-label:${GmailLabel.INBOX}`);
}

if (excludeLabelNames) {
queryParts.push(`-in:"${excludeLabelNames.join(" ")}"`);
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
Comment thread
elie222 marked this conversation as resolved.
Outdated
}

Comment thread
elie222 marked this conversation as resolved.
return queryParts.length > 0 ? queryParts.join(" ") : undefined;
}

function getLabelIds(type?: string | null) {
if (labelIds) {
return labelIds;
}

switch (type) {
case "inbox":
return [GmailLabel.INBOX];
Expand Down Expand Up @@ -673,9 +690,7 @@ export class GmailProvider implements EmailProvider {
await getThreadsWithNextPageToken({
gmail: this.client,
q: getQuery(),
labelIds: query?.labelId
? [query.labelId]
: getLabelIds(query?.type) || [],
labelIds: labelId ? [labelId] : getLabelIds(type) || [],
maxResults: options.maxResults || 50,
pageToken: options.pageToken || undefined,
});
Expand Down
Loading
Loading