Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
588ab02
add: add dockerfile
Muhammad-Owais-Warsi Apr 5, 2025
1347a8e
Merge branch 'Mail-0:staging' into add/dockerfile
Muhammad-Owais-Warsi Apr 7, 2025
373423b
add: docker-compose.yaml
Muhammad-Owais-Warsi Apr 7, 2025
cd8833b
Merge branch 'add/dockerfile' of https://github.com/Muhammad-Owais-Wa…
Muhammad-Owais-Warsi Apr 7, 2025
49acc64
cleanup
hiheyhello123 Apr 8, 2025
8dba8a9
cleanup
hiheyhello123 Apr 8, 2025
7dd0fcd
Merge branch 'hotkeys' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 9, 2025
0d4be12
update dockerfile.
Muhammad-Owais-Warsi Apr 9, 2025
e35bd3f
Merge branch 'Mail-0:main' into add/dockerfile
Muhammad-Owais-Warsi Apr 9, 2025
d1bcb68
- switched to react-hotkeys-hook
ahmetskilinc Apr 9, 2025
f9f16db
add: docker-compose.yaml
Muhammad-Owais-Warsi Apr 9, 2025
e730552
Merge branch 'staging' into add/dockerfile
Muhammad-Owais-Warsi Apr 9, 2025
85e8fbf
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 10, 2025
dd9f236
feat: add farsi support
essinn Apr 10, 2025
2398a61
Merge branch 'main' into feature/language-support-fa
MrgSub Apr 11, 2025
93eb1d2
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
814fc48
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
749cabc
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
4d51e20
Merge branch 'main' into feature/language-support-fa
nizzyabi Apr 11, 2025
d3c72b5
hotkeys
hiheyhello123 Apr 12, 2025
972ddd0
Merge branch 'staging' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 12, 2025
4ee76c3
feat: save hotkeys to indexedDB
ahmetskilinc Apr 12, 2025
38d85e4
remove console logs from shortcuts
ahmetskilinc Apr 12, 2025
8f687d7
fix: load all shortcuts by default
ahmetskilinc Apr 12, 2025
458b065
fix: add key for selectAll in en.json
ahmetskilinc Apr 12, 2025
8a0a833
hotkeys db and api route
ahmetskilinc Apr 13, 2025
ca1bc22
fix hotkeys db loading too many times
ahmetskilinc Apr 13, 2025
a864044
remove comments
ahmetskilinc Apr 13, 2025
03106ce
feat: update fa.json to empty string and add farsi to i18n.json
essinn Apr 14, 2025
b73d3be
Merge branch 'feature/language-support-fa' of https://github.com/essi…
essinn Apr 14, 2025
a71f0c4
Merge branch 'main' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 14, 2025
9e22918
fix: remove unused imports
ahmetskilinc Apr 14, 2025
db58b12
fix: better handling of indexdb and syncing with postgres db
ahmetskilinc Apr 14, 2025
9313985
remove comments from hotkeys files
ahmetskilinc Apr 14, 2025
14dfeed
Merge branch 'main' into new-keybinds
MrgSub Apr 17, 2025
2bdcd8d
Refactor email creation component imports and structure
MrgSub Apr 17, 2025
cffc4f6
Merge pull request #649 from Mail-0/new-keybinds
MrgSub Apr 17, 2025
870dca9
Merge branch 'staging' into feature/language-support-fa
MrgSub Apr 17, 2025
32a698d
Merge pull request #636 from essinn/feature/language-support-fa
MrgSub Apr 17, 2025
576cc73
Merge branch 'staging' into add/dockerfile
MrgSub Apr 17, 2025
c82605a
Merge pull request #591 from Muhammad-Owais-Warsi/add/dockerfile
MrgSub Apr 17, 2025
0e2d67a
cleanup
hiheyhello123 Apr 17, 2025
61a4304
golden ticket fit
nizzyabi Apr 17, 2025
5945c96
Refactor API functions to handle unauthorized gracefully
MrgSub Apr 17, 2025
e902575
Refactor error handling in GetSummary function
MrgSub Apr 17, 2025
7d094ad
Fix handling of missing BRAIN_URL in EnableBrain function
MrgSub Apr 17, 2025
c47a453
Handle error in redirect function call
MrgSub Apr 17, 2025
bbb5c15
Improve error handling in throwUnauthorizedGracefully
MrgSub Apr 17, 2025
45a35d1
Merge branch 'main' into staging
MrgSub Apr 17, 2025
1ef5490
Refactor error handling in draft and mail actions
MrgSub Apr 17, 2025
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: 6 additions & 5 deletions apps/mail/actions/ai-reply.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use server';

import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { generateCompletions } from '@/lib/groq';
import { headers } from 'next/headers';
import { auth } from '@/lib/auth';
import { generateCompletions } from '@/lib/groq';

// Generates an AI response for an email reply based on the thread content
export async function generateAIResponse(
Expand All @@ -13,15 +14,15 @@ export async function generateAIResponse(
const session = await auth.api.getSession({ headers: headersList });

if (!session?.user) {
throw new Error('Unauthorized');
return throwUnauthorizedGracefully();
}

if (!process.env.GROQ_API_KEY) {
throw new Error('Groq API key is not configured');
}

// Use a more aggressive content reduction approach
const processedContent = threadContent
const processedContent = threadContent;

// Create the system message
const systemPrompt = `You are an email assistant helping ${session.user.name} write professional and concise email replies.
Expand All @@ -44,7 +45,7 @@ export async function generateAIResponse(
- Double space paragraphs (2 newlines)
- Add two spaces below the sign-off
- End with the name: ${session.user.name}`;

// Create the user message - keep it shorter
const prompt = `
Here's the context of the email thread (some parts may be summarized or truncated due to length):
Expand Down Expand Up @@ -82,7 +83,7 @@ export async function generateAIResponse(
systemPrompt: process.env.AI_SYSTEM_PROMPT || systemPrompt,
prompt,
temperature: 0.6,
userName: session.user.name
userName: session.user.name,
});

return completion;
Expand Down
7 changes: 4 additions & 3 deletions apps/mail/actions/ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// The brain.ts file in /actions should replace this file once ready.
'use server';

import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { generateEmailContent } from '@/lib/ai';
import { headers } from 'next/headers';
import { JSONContent } from 'novel';
Expand Down Expand Up @@ -37,9 +38,9 @@ export async function generateAIEmailContent({
const session = await auth.api.getSession({ headers: headersList });

if (!session?.user) {
throw new Error('Unauthorized');
return throwUnauthorizedGracefully();
}
Comment on lines 40 to 42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify return type compatibility with function signature.

The function is declared to return Promise<AIEmailResponse>, but now it can return the result of throwUnauthorizedGracefully() which redirects to the login page. This creates a potential type inconsistency.


🏁 Script executed:

#!/bin/bash
# Check what throwUnauthorizedGracefully actually returns and if it's compatible with AIEmailResponse
grep -r "throwUnauthorizedGracefully" --include="*.ts" apps/mail/app/api/utils.ts

Length of output: 180


🏁 Script executed:

#!/bin/bash
# Show full definition of throwUnauthorizedGracefully for return type inspection
rg -C 5 "export const throwUnauthorizedGracefully" apps/mail/app/api/utils.ts

Length of output: 367


Ensure return type consistency for the unauthorized flow

The ai.ts action is declared to return Promise<AIEmailResponse>, but on an unauthenticated session you’re doing:

if (!session?.user) {
  return throwUnauthorizedGracefully();
}

Here throwUnauthorizedGracefully() is an async helper that ultimately calls redirect() (which throws) and otherwise returns Promise<void>, not Promise<AIEmailResponse>. This mismatch can lead to subtle runtime or type‑checking issues.

To fix this, choose one of the following:

  • Change your action’s signature to allow the redirect branch explicitly, e.g.
    Promise<AIEmailResponse>, or Promise<AIEmailResponse | void>, so TS knows you may not return a payload when redirecting.
  • Remove the return, call it with await throwUnauthorizedGracefully(); (so the action never actually returns), or
  • Modify throwUnauthorizedGracefully() to return or throw a value that satisfies AIEmailResponse (e.g. throw a typed exception or return a dummy response), ensuring all code paths adhere to AIEmailResponse.

Files/locations to review:

  • apps/mail/actions/ai.ts → lines 40–42 (unauthenticated check)
  • apps/mail/app/api/utils.ts → throwUnauthorizedGracefully return type


const responses = await generateEmailContent(
prompt,
currentContent,
Expand All @@ -58,7 +59,7 @@ export async function generateAIEmailContent({
}

const emailResponses = responses.filter((r) => r.type === 'email');

const cleanedContent = emailResponses
.map((r) => r.content)
.join('\n\n')
Expand Down
34 changes: 15 additions & 19 deletions apps/mail/actions/brain.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
'use server'
import { auth } from "@/lib/auth";
import { db } from "@zero/db";
import { connection } from "@zero/db/schema";
import { headers } from "next/headers";
import { and, eq } from "drizzle-orm";
import axios from "axios";
import { getActiveConnection } from "./utils";
'use server';
import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { getActiveConnection } from './utils';
import axios from 'axios';

export const EnableBrain = async () => {
if (!process.env.BRAIN_URL) {
throw new Error('Brain URL not found');
}
if (!process.env.BRAIN_URL) {
return null;
}

const connection = await getActiveConnection()
const connection = await getActiveConnection();

if (!connection?.accessToken || !connection.refreshToken) {
throw new Error("Unauthorized, reconnect");
}
if (!connection?.accessToken || !connection.refreshToken) {
return throwUnauthorizedGracefully();
}

return await axios.put(process.env.BRAIN_URL + `/subscribe/${connection.providerId}`, {
connectionId: connection.id,
})
}
return await axios.put(process.env.BRAIN_URL + `/subscribe/${connection.providerId}`, {
connectionId: connection.id,
});
};
10 changes: 5 additions & 5 deletions apps/mail/actions/connections.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use server';

import { getAuthenticatedUserId } from '@/app/api/utils';
import { getAuthenticatedUserId, throwUnauthorizedGracefully } from '@/app/api/utils';
import { connection, user } from '@zero/db/schema';
import { type IConnection } from '@/types';
import { headers } from 'next/headers';
Expand All @@ -14,13 +14,13 @@ export async function deleteConnection(connectionId: string) {
const session = await auth.api.getSession({ headers: headersList });

if (!session) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

const userId = session?.user?.id;

if (!userId) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

await db
Expand All @@ -46,13 +46,13 @@ export async function putConnection(connectionId: string) {
const session = await auth.api.getSession({ headers: headersList });

if (!session) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

const userId = session?.user?.id;

if (!userId) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

const [foundConnection] = await db
Expand Down
18 changes: 10 additions & 8 deletions apps/mail/actions/drafts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use server";
import { getActiveDriver } from "./utils";
'use server';
import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { getActiveDriver } from './utils';

export const getDrafts = async ({
q,
Expand All @@ -14,8 +15,9 @@ export const getDrafts = async ({
const driver = await getActiveDriver();
return await driver.listDrafts(q, max, pageToken);
} catch (error) {
console.error("Error getting threads:", error);
throw error;
console.error('Error getting threads:', error);
await throwUnauthorizedGracefully();
// throw error;
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improved error handling but missing return value

The error handling now correctly uses the centralized utility for graceful handling of unauthorized errors. However, unlike in mail.ts where an empty array is returned, here there's no explicit return value after calling throwUnauthorizedGracefully().

This could result in an undefined return value when an error occurs, potentially causing issues in the UI. Consider adding an appropriate return value:

  try {
    const driver = await getActiveDriver();
    return await driver.listDrafts(q, max, pageToken);
  } catch (error) {
    console.error('Error getting threads:', error);
    await throwUnauthorizedGracefully();
    // throw error;
+   return { messages: [], nextPageToken: null };
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error('Error getting threads:', error);
await throwUnauthorizedGracefully();
// throw error;
console.error('Error getting threads:', error);
await throwUnauthorizedGracefully();
// throw error;
return { messages: [], nextPageToken: null };

}
};

Expand All @@ -27,23 +29,23 @@ export const createDraft = async (data: any) => {

return { success: true, id: res.id };
} catch (error) {
console.error("Error creating draft:", error);
console.error('Error creating draft:', error);
return { success: false, error: String(error) };
}
};

export const getDraft = async (id: string) => {
if (!id) {
throw new Error("Missing draft ID");
throw new Error('Missing draft ID');
}

console.log("getting email:", id);
console.log('getting email:', id);

try {
const driver = await getActiveDriver();
return await driver.getDraft(id);
} catch (error) {
console.error("Error getting draft:", error);
console.error('Error getting draft:', error);
throw error;
}
};
11 changes: 5 additions & 6 deletions apps/mail/actions/getSummary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use server';
import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { connection, summary } from '@zero/db/schema';
import { headers } from 'next/headers';
import { and, eq } from 'drizzle-orm';
Expand All @@ -11,7 +12,7 @@ export const GetSummary = async (threadId: string) => {
const session = await auth.api.getSession({ headers: headersList });

if (!session || !session.connectionId) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

const [_connection] = await db
Expand All @@ -20,23 +21,21 @@ export const GetSummary = async (threadId: string) => {
.where(and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId)));

if (!_connection) {
throw new Error('Unauthorized, reconnect');
return throwUnauthorizedGracefully();
}

try {
if (!process.env.BRAIN_URL || process.env.NODE_ENV !== 'production') {
throw new Error('Brain URL not found');
return null;
}

const response = await axios
.get(process.env.BRAIN_URL + `/brain/message/thread/${threadId}`)
.then((e) => e.data);

console.log('Summary:', response);

return response?.content ?? null;
} catch (error) {
console.error('Error getting summary:', error);
// console.error('Error getting summary:', error);
return null;
}
};
5 changes: 4 additions & 1 deletion apps/mail/actions/mail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use server';
import { deleteActiveConnection, FatalErrors, getActiveDriver } from './utils';
import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { IGetThreadResponse } from '@/app/api/driver/types';
import { ParsedMessage } from '@/types';

Expand All @@ -26,7 +27,9 @@ export const getMails = async ({
} catch (error) {
if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection();
console.error('Error getting threads:', error);
throw error;
// throw error;
await throwUnauthorizedGracefully();
return [];
}
};

Expand Down
25 changes: 12 additions & 13 deletions apps/mail/actions/send.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use server";
'use server';

import { createDriver } from "@/app/api/driver";
import { Sender } from "@/types";
import { getActiveConnection } from "./utils";
import { throwUnauthorizedGracefully } from '@/app/api/utils';
import { createDriver } from '@/app/api/driver';
import { getActiveConnection } from './utils';
import { Sender } from '@/types';

export async function sendEmail({
to,
Expand All @@ -12,7 +13,7 @@ export async function sendEmail({
bcc,
cc,
headers: additionalHeaders = {},
threadId
threadId,
}: {
to: Sender[];
subject: string;
Expand All @@ -21,25 +22,23 @@ export async function sendEmail({
headers?: Record<string, string>;
cc?: Sender[];
bcc?: Sender[];
threadId?: string
threadId?: string;
}) {
if (!to || !subject || !message) {
throw new Error("Missing required fields");
throw new Error('Missing required fields');
}

console.log(additionalHeaders)

const connection = await getActiveConnection()
const connection = await getActiveConnection();

if (!connection?.accessToken || !connection.refreshToken) {
throw new Error("Unauthorized, reconnect");
return throwUnauthorizedGracefully();
}

const driver = await createDriver(connection.providerId, {
auth: {
access_token: connection.accessToken,
refresh_token: connection.refreshToken,
email: connection.email
email: connection.email,
},
});

Expand All @@ -51,7 +50,7 @@ export async function sendEmail({
headers: additionalHeaders,
cc,
bcc,
threadId
threadId,
});

return { success: true };
Expand Down
Loading