diff --git a/apps/mail/actions/ai-reply.ts b/apps/mail/actions/ai-reply.ts index f907bba7ba..6d2544ece8 100644 --- a/apps/mail/actions/ai-reply.ts +++ b/apps/mail/actions/ai-reply.ts @@ -14,7 +14,7 @@ export async function generateAIResponse( const session = await auth.api.getSession({ headers: headersList }); if (!session?.user) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } if (!process.env.GROQ_API_KEY) { diff --git a/apps/mail/actions/brain.ts b/apps/mail/actions/brain.ts index e53568a433..f1d07a1e5f 100644 --- a/apps/mail/actions/brain.ts +++ b/apps/mail/actions/brain.ts @@ -11,7 +11,7 @@ export const EnableBrain = async () => { const connection = await getActiveConnection(); if (!connection?.accessToken || !connection.refreshToken) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } return await axios.put(process.env.BRAIN_URL + `/subscribe/${connection.providerId}`, { diff --git a/apps/mail/actions/connections.ts b/apps/mail/actions/connections.ts index d289e9f077..f82a534bcb 100644 --- a/apps/mail/actions/connections.ts +++ b/apps/mail/actions/connections.ts @@ -14,13 +14,13 @@ export async function deleteConnection(connectionId: string) { const session = await auth.api.getSession({ headers: headersList }); if (!session) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const userId = session?.user?.id; if (!userId) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } await db @@ -46,13 +46,13 @@ export async function putConnection(connectionId: string) { const session = await auth.api.getSession({ headers: headersList }); if (!session) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const userId = session?.user?.id; if (!userId) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const [foundConnection] = await db diff --git a/apps/mail/actions/drafts.ts b/apps/mail/actions/drafts.ts index 365ccead9a..34acde3111 100644 --- a/apps/mail/actions/drafts.ts +++ b/apps/mail/actions/drafts.ts @@ -16,9 +16,7 @@ export const getDrafts = async ({ return await driver.listDrafts(q, max, pageToken); } catch (error) { console.error('Error getting threads:', error); - await throwUnauthorizedGracefully(); - // throw error; - return { messages: [], nextPageToken: null }; + return throwUnauthorizedGracefully(); } }; diff --git a/apps/mail/actions/email-aliases.ts b/apps/mail/actions/email-aliases.ts deleted file mode 100644 index 23abaf9432..0000000000 --- a/apps/mail/actions/email-aliases.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use server'; - -import { throwUnauthorizedGracefully } from '@/app/api/utils'; -import { createDriver } from '@/app/api/driver'; -import { getActiveConnection } from './utils'; - -export type EmailAlias = { - email: string; - name?: string; - primary?: boolean; -}; - -export async function getEmailAliases(): Promise { - const connection = await getActiveConnection(); - - if (!connection?.accessToken || !connection.refreshToken) { - console.error('Unauthorized: No valid connection found'); - return []; - } - - const driver = await createDriver(connection.providerId, { - auth: { - access_token: connection.accessToken, - refresh_token: connection.refreshToken, - email: connection.email, - }, - }); - - try { - const aliases = await driver.getEmailAliases(); - return aliases; - } catch (error) { - console.error('Error fetching email aliases:', error); - return [{ email: connection.email, primary: true }]; - } -} diff --git a/apps/mail/actions/getSummary.ts b/apps/mail/actions/getSummary.ts index 2a67d6db33..00f47e177e 100644 --- a/apps/mail/actions/getSummary.ts +++ b/apps/mail/actions/getSummary.ts @@ -12,7 +12,7 @@ export const GetSummary = async (threadId: string) => { const session = await auth.api.getSession({ headers: headersList }); if (!session || !session.connectionId) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const [_connection] = await db @@ -21,7 +21,7 @@ export const GetSummary = async (threadId: string) => { .where(and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId))); if (!_connection) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } try { diff --git a/apps/mail/actions/mail.ts b/apps/mail/actions/mail.ts index e65a10f442..baf8476f65 100644 --- a/apps/mail/actions/mail.ts +++ b/apps/mail/actions/mail.ts @@ -1,38 +1,9 @@ 'use server'; -import { deleteActiveConnection, FatalErrors, getActiveDriver } from './utils'; import { throwUnauthorizedGracefully } from '@/app/api/utils'; import { IGetThreadResponse } from '@/app/api/driver/types'; +import { FatalErrors, getActiveDriver } from './utils'; import { ParsedMessage } from '@/types'; -export const getMails = async ({ - folder, - q, - max, - labelIds, - pageToken, -}: { - folder: string; - q?: string; - max?: number; - labelIds?: string[]; - pageToken: string | number | undefined; -}) => { - if (!folder) { - throw new Error('Missing required fields'); - } - - try { - const driver = await getActiveDriver(); - return await driver.list(folder, q, max, labelIds, pageToken); - } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); - console.error('Error getting threads:', error); - // throw error; - await throwUnauthorizedGracefully(); - return { messages: [], nextPageToken: null }; - } -}; - export const getMail = async ({ id }: { id: string }): Promise => { if (!id) { throw new Error('Missing required fields'); @@ -47,7 +18,7 @@ export const getMail = async ({ id }: { id: string }): Promise { await driver.markAsRead(ids); return { success: true }; } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); + if (FatalErrors.includes((error as Error).message)) await throwUnauthorizedGracefully(); console.error('Error marking message as read:', error); throw error; } @@ -71,23 +42,12 @@ export const markAsUnread = async ({ ids }: { ids: string[] }) => { await driver.markAsUnread(ids); return { success: true }; } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); + if (FatalErrors.includes((error as Error).message)) await throwUnauthorizedGracefully(); console.error('Error marking message as unread:', error); throw error; } }; -export const mailCount = async () => { - try { - const driver = await getActiveDriver(); - return await driver.count(); - } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); - console.error('Error getting mail count:', error); - throw error; - } -}; - export const modifyLabels = async ({ threadId, addLabels = [], @@ -117,7 +77,7 @@ export const modifyLabels = async ({ console.log('Server: No label changes specified'); return { success: false, error: 'No label changes specified' }; } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); + if (FatalErrors.includes((error as Error).message)) await throwUnauthorizedGracefully(); console.error('Error updating thread labels:', error); throw error; } @@ -159,7 +119,7 @@ export const toggleStar = async ({ ids }: { ids: string[] }) => { return { success: true }; } catch (error) { - if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection(); + if (FatalErrors.includes((error as Error).message)) await throwUnauthorizedGracefully(); console.error('Error toggling star:', error); throw error; } diff --git a/apps/mail/actions/send.ts b/apps/mail/actions/send.ts index 973505a03c..7cb2813b22 100644 --- a/apps/mail/actions/send.ts +++ b/apps/mail/actions/send.ts @@ -33,7 +33,7 @@ export async function sendEmail({ const connection = await getActiveConnection(); if (!connection?.accessToken || !connection.refreshToken) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const driver = await createDriver(connection.providerId, { diff --git a/apps/mail/actions/settings.ts b/apps/mail/actions/settings.ts index 2d3aaa2ac6..dbfff268ec 100644 --- a/apps/mail/actions/settings.ts +++ b/apps/mail/actions/settings.ts @@ -1,10 +1,9 @@ 'use server'; import { type UserSettings, userSettingsSchema } from '@zero/db/user_settings_default'; -import { earlyAccess, user, userSettings } from '@zero/db/schema'; import { getAuthenticatedUserId } from '@/app/api/utils'; +import { userSettings } from '@zero/db/schema'; import { eq } from 'drizzle-orm'; -import { Resend } from 'resend'; import { db } from '@zero/db'; function validateSettings(settings: unknown): UserSettings { @@ -60,218 +59,3 @@ export async function saveUserSettings(settings: UserSettings) { throw new Error('Failed to save user settings'); } } - -export async function handleGoldenTicket(email: string) { - try { - const userId = await getAuthenticatedUserId(); - const [foundUser] = await db - .select({ - hasUsedTicket: earlyAccess.hasUsedTicket, - email: user.email, - isEarlyAccess: earlyAccess.isEarlyAccess, - }) - .from(user) - .leftJoin(earlyAccess, eq(user.email, earlyAccess.email)) - .where(eq(user.id, userId)) - .limit(1); - - if (!foundUser) { - return { success: false, error: 'User not found' }; - } - - if (foundUser.hasUsedTicket) { - return { success: false, error: 'Golden ticket already claimed' }; - } - - if (!foundUser.isEarlyAccess) { - return { success: false, error: 'Unauthorized' }; - } - - const sendNotification = () => { - return resend.emails.send({ - from: '0.email ', - to: email, - subject: 'You <> Zero', - html: ` - - - - - - - -
- You've been granted early access to Zero! -
- ‌​‍‎‏ -
-
- - - - - - -
- - - - - - -
- Zero Early Access -
-

- Welcome to Zero Early Access! -

-

- Hi there, -

-

- Your friend has invited you to join Zero! We're excited to have you on board. Click the button below to get started. -

- - - - - - -
- Access Zero Now -
-

- Join our - Discord community - to connect with other early users and the Zero team for support, - feedback, and exclusive updates. -

-

- Your feedback during this early access phase is invaluable to us - as we continue to refine and improve Zero. We can't wait to - hear what you think! -

-
-

- © - 2025 - Zero Email Inc. All rights reserved. -

-
- -`, - }); - }; - - await db - .insert(earlyAccess) - .values({ - id: crypto.randomUUID(), - email, - createdAt: new Date(), - updatedAt: new Date(), - isEarlyAccess: true, - hasUsedTicket: '', - }) - .catch(async (error) => { - console.log('Error registering early access', error); - if (error.code === '23505') { - console.log('Email already registered for early access, granted access'); - await db - .update(earlyAccess) - .set({ - hasUsedTicket: '', - updatedAt: new Date(), - isEarlyAccess: true, - }) - .where(eq(earlyAccess.email, email)); - } else { - console.error('Error registering early access', error); - await db - .update(earlyAccess) - .set({ - hasUsedTicket: email, - updatedAt: new Date(), - }) - .where(eq(earlyAccess.email, foundUser.email)) - .catch((err) => { - console.error('Error updating early access', err); - }); - await sendNotification(); - throw error; - } - }); - - await db - .update(earlyAccess) - .set({ - hasUsedTicket: email, - updatedAt: new Date(), - }) - .where(eq(earlyAccess.email, foundUser.email)); - - const resend = process.env.RESEND_API_KEY - ? new Resend(process.env.RESEND_API_KEY) - : { emails: { send: async (...args: any[]) => console.log(args) } }; - - await sendNotification(); - - return { success: true }; - } catch (error) { - console.error('Failed to handle golden ticket:', error); - throw new Error('Failed to handle golden ticket'); - } -} diff --git a/apps/mail/actions/utils.ts b/apps/mail/actions/utils.ts index 443d332cdd..c26834102f 100644 --- a/apps/mail/actions/utils.ts +++ b/apps/mail/actions/utils.ts @@ -12,20 +12,20 @@ export const FatalErrors = ['invalid_grant']; export const deleteActiveConnection = async () => { const headersList = await headers(); const session = await auth.api.getSession({ headers: headersList }); - if (!session || !session.connectionId) { - return throwUnauthorizedGracefully(); - } - - try { - await db - .delete(connection) - .where(and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId))); - console.log('Server: Successfully deleted connection, please reload'); - return revalidatePath('/mail'); - } catch (error) { - console.error('Server: Error deleting connection:', error); - throw error; - } + if (session?.connectionId) + try { + await db + .delete(connection) + .where( + and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId)), + ); + console.log('Server: Successfully deleted connection, please reload'); + await auth.api.signOut({ headers: headersList }); + // return revalidatePath('/mail'); + } catch (error) { + console.error('Server: Error deleting connection:', error); + throw error; + } }; export const getActiveDriver = async () => { @@ -33,7 +33,7 @@ export const getActiveDriver = async () => { const session = await auth.api.getSession({ headers: headersList }); if (!session || !session.connectionId) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const [_connection] = await db @@ -42,11 +42,11 @@ export const getActiveDriver = async () => { .where(and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId))); if (!_connection) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } if (!_connection.accessToken || !_connection.refreshToken) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } const driver = await createDriver(_connection.providerId, { @@ -64,8 +64,8 @@ export const getActiveConnection = async () => { const headersList = await headers(); const session = await auth.api.getSession({ headers: headersList }); - if (!session?.user) return throwUnauthorizedGracefully(); - if (!session.connectionId) return throwUnauthorizedGracefully(); + if (!session?.user) return throwUnauthorizedGracefully() as never; + if (!session.connectionId) return throwUnauthorizedGracefully() as never; const [_connection] = await db .select() diff --git a/apps/mail/app/(routes)/mail/create/page.tsx b/apps/mail/app/(routes)/mail/create/page.tsx index c3df6f2822..08651411fc 100644 --- a/apps/mail/app/(routes)/mail/create/page.tsx +++ b/apps/mail/app/(routes)/mail/create/page.tsx @@ -3,6 +3,15 @@ import { auth } from '@/lib/auth'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; +// Define the type for search params +interface CreatePageProps { + searchParams: Promise<{ + to?: string; + subject?: string; + body?: string; + }>; +} + export default async function CreatePage() { const headersList = await headers(); const session = await auth.api.getSession({ headers: headersList }); @@ -19,3 +28,32 @@ export default async function CreatePage() { ); } + +export async function generateMetadata({ searchParams }: CreatePageProps) { + // Need to await searchParams in Next.js 15+ + const params = await searchParams; + + const toParam = params.to || 'someone'; + + // Create common metadata properties + const title = `Email ${toParam} on Zero`; + const description = 'Zero - The future of email is here'; + const imageUrl = `/api/og/create?to=${encodeURIComponent(toParam)}${params.subject ? `&subject=${encodeURIComponent(params.subject)}` : ''}`; + + // Create metadata object + return { + title, + description, + openGraph: { + title, + description, + images: [imageUrl], + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: [imageUrl], + } + }; +} diff --git a/apps/mail/app/api/driver/connections/route.ts b/apps/mail/app/api/driver/connections/route.ts index 390a2b300d..6f06416cc3 100644 --- a/apps/mail/app/api/driver/connections/route.ts +++ b/apps/mail/app/api/driver/connections/route.ts @@ -3,7 +3,7 @@ import { getRatelimitModule, checkRateLimit, getAuthenticatedUserId, - logoutUser, + throwUnauthorizedGracefully, } from '../../utils'; import { NextRequest, NextResponse } from 'next/server'; import { Ratelimit } from '@upstash/ratelimit'; @@ -44,8 +44,8 @@ export const GET = async (req: NextRequest) => { headers, }); } catch (error) { - console.warn('Error getting connections:', error); - await logoutUser(); - return NextResponse.json([]); + console.log('Error getting connections:', error); + await throwUnauthorizedGracefully(req); + return NextResponse.redirect(`https://${req.nextUrl.hostname}`); } }; diff --git a/apps/mail/app/api/driver/count/route.ts b/apps/mail/app/api/driver/count/route.ts index a18069161d..a3fcb805e9 100644 --- a/apps/mail/app/api/driver/count/route.ts +++ b/apps/mail/app/api/driver/count/route.ts @@ -2,8 +2,8 @@ import { checkRateLimit, getAuthenticatedUserId, getRatelimitModule, - logoutUser, processIP, + throwUnauthorizedGracefully, } from '../../utils'; import { type NextRequest, NextResponse } from 'next/server'; import { getActiveDriver } from '@/actions/utils'; @@ -31,8 +31,8 @@ export const GET = async (req: NextRequest) => { headers, }); } catch (error) { - console.warn('Error getting count:', error); - await logoutUser(); - return NextResponse.json({}); + console.log('Error getting connections:', error); + await throwUnauthorizedGracefully(req); + return NextResponse.redirect(`https://${req.nextUrl.hostname}`); } }; diff --git a/apps/mail/app/api/driver/notes/route.ts b/apps/mail/app/api/driver/notes/route.ts index 27885d44ff..820227e93b 100644 --- a/apps/mail/app/api/driver/notes/route.ts +++ b/apps/mail/app/api/driver/notes/route.ts @@ -3,7 +3,7 @@ import { getRatelimitModule, checkRateLimit, getAuthenticatedUserId, - logoutUser, + throwUnauthorizedGracefully, } from '../../utils'; import { NextRequest, NextResponse } from 'next/server'; import { fetchThreadNotes } from '@/actions/notes'; @@ -39,7 +39,7 @@ export const GET = async (req: NextRequest) => { }); } catch (error) { console.warn('Error getting thread notes:', error); - await logoutUser(); - return NextResponse.json([]); + } finally { + throwUnauthorizedGracefully(req); } }; diff --git a/apps/mail/app/api/driver/route.ts b/apps/mail/app/api/driver/route.ts index 71d536589e..0cd2280d0c 100644 --- a/apps/mail/app/api/driver/route.ts +++ b/apps/mail/app/api/driver/route.ts @@ -2,8 +2,8 @@ import { checkRateLimit, getAuthenticatedUserId, getRatelimitModule, - logoutUser, processIP, + throwUnauthorizedGracefully, } from '../utils'; import { type NextRequest, NextResponse } from 'next/server'; import { getActiveDriver } from '@/actions/utils'; @@ -43,8 +43,8 @@ export const GET = async (req: NextRequest) => { headers, }); } catch (error) { - console.warn('Error getting threads:', error); - await logoutUser(); - return NextResponse.json({ messages: [], nextPageToken: null }); + console.log('Error getting threads:', error); + await throwUnauthorizedGracefully(req); + return NextResponse.redirect(`https://${req.nextUrl.hostname}`); } }; diff --git a/apps/mail/app/api/notes/index.ts b/apps/mail/app/api/notes/index.ts index 84e751fd00..729a350733 100644 --- a/apps/mail/app/api/notes/index.ts +++ b/apps/mail/app/api/notes/index.ts @@ -11,7 +11,7 @@ async function getCurrentUserId(): Promise { const session = await auth.api.getSession({ headers: headersList }); if (!session?.user?.id) { - return throwUnauthorizedGracefully(); + return throwUnauthorizedGracefully() as never; } return session.user.id; } catch (error) { diff --git a/apps/mail/app/api/og/create/route.tsx b/apps/mail/app/api/og/create/route.tsx new file mode 100644 index 0000000000..6e045510b7 --- /dev/null +++ b/apps/mail/app/api/og/create/route.tsx @@ -0,0 +1,116 @@ +import { ImageResponse } from 'next/og'; + +export const runtime = 'edge'; + +export async function GET(request: Request) { + // Get URL parameters + const { searchParams } = new URL(request.url); + const toParam = searchParams.get('to') || 'someone'; + const subjectParam = searchParams.get('subject') || ''; + + // Use the email directly + const recipient = toParam; + + // Load fonts + async function loadGoogleFont(font: string, weight: string) { + const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}&display=swap`; + const css = await (await fetch(url)).text(); + const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/); + + // Check if resource and the captured group exist + if (resource?.[1]) { + const response = await fetch(resource[1]); + if (response.status === 200) { + return await response.arrayBuffer(); + } + } + + throw new Error('failed to load font data'); + } + + // Use a simple embedded SVG for the Zero logo instead of trying to load from file + const logoSvg = ` + + `; + + const logoDataUrl = `data:image/svg+xml;base64,${Buffer.from(logoSvg).toString('base64')}`; + + const fontWeight400 = await loadGoogleFont('Geist', '400'); + const fontWeight600 = await loadGoogleFont('Geist', '600'); + + return new ImageResponse( + ( +
+
+
+ mail +
+
+
+ Email + {recipient} + on Zero +
+ +
+ +
+ {subjectParam + ? `Subject: ${subjectParam.length > 50 ? subjectParam.substring(0, 47) + '...' : subjectParam}` + : 'Compose a new email'} +
+
+ +
+
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'light', + data: fontWeight400, + style: 'normal', + weight: 400, + }, + { + name: 'bold', + data: fontWeight600, + style: 'normal', + weight: 600, + }, + ], + }, + ); +} \ No newline at end of file diff --git a/apps/mail/app/api/og/home/route.tsx b/apps/mail/app/api/og/home/route.tsx index dcbcf43861..217c7a876e 100644 --- a/apps/mail/app/api/og/home/route.tsx +++ b/apps/mail/app/api/og/home/route.tsx @@ -8,7 +8,7 @@ export async function GET() { const css = await (await fetch(url)).text(); const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/); - if (resource) { + if (resource?.[1]) { const response = await fetch(resource[1]); if (response.status === 200) { return await response.arrayBuffer(); diff --git a/apps/mail/app/api/utils.ts b/apps/mail/app/api/utils.ts index fcdfd8c722..63d1868cc8 100644 --- a/apps/mail/app/api/utils.ts +++ b/apps/mail/app/api/utils.ts @@ -1,7 +1,7 @@ import { Ratelimit, Algorithm, RatelimitConfig } from '@upstash/ratelimit'; import { deleteActiveConnection } from '@/actions/utils'; +import { NextRequest, NextResponse } from 'next/server'; import { redirect } from 'next/navigation'; -import { NextRequest } from 'next/server'; import { headers } from 'next/headers'; import { redis } from '@/lib/redis'; import { auth } from '@/lib/auth'; @@ -20,16 +20,15 @@ export const getRatelimitModule = (config: { return ratelimit; }; -export const throwUnauthorizedGracefully = async () => { +export const throwUnauthorizedGracefully = async (req?: NextRequest) => { console.warn('Unauthorized, redirecting to login'); - try { - const headersList = await headers(); - await auth.api.signOut({ headers: headersList }); - throw new Error('Unauthorized'); - } catch (error) { - console.warn('Error signing out & redirecting to login:', error); - throw error; - } + await deleteActiveConnection(); + // const headersList = await headers(); + // await auth.api.signOut({ headers: headersList }); + // if (req) { + // return NextResponse.redirect(`https://${req.nextUrl.hostname}`); + // } + // throw redirect('/err'); }; export async function getAuthenticatedUserId(): Promise { @@ -37,19 +36,13 @@ export async function getAuthenticatedUserId(): Promise { const session = await auth.api.getSession({ headers: headersList }); if (!session?.user?.id) { - return throwUnauthorizedGracefully(); + console.log('No user ID found', session); + return throwUnauthorizedGracefully() as never; } return session.user.id; } -// Forcefully logout the user, this will delete the active connection -export async function logoutUser() { - await deleteActiveConnection(); - const headersList = await headers(); - await auth.api.signOut({ headers: headersList }); -} - export const checkRateLimit = async (ratelimit: Ratelimit, finalIp: string) => { const { success, limit, reset, remaining } = await ratelimit.limit(finalIp); const headers = { diff --git a/apps/mail/app/api/v1/email-aliases/route.ts b/apps/mail/app/api/v1/email-aliases/route.ts new file mode 100644 index 0000000000..f9ab7a2669 --- /dev/null +++ b/apps/mail/app/api/v1/email-aliases/route.ts @@ -0,0 +1,27 @@ +import { getActiveConnection } from '@/actions/utils'; +import { createDriver } from '@/app/api/driver'; +import { NextResponse } from 'next/server'; + +export async function GET() { + try { + const connection = await getActiveConnection(); + + if (!connection?.accessToken || !connection.refreshToken) { + console.error('Unauthorized: No valid connection found'); + return []; + } + + const driver = await createDriver(connection.providerId, { + auth: { + access_token: connection.accessToken, + refresh_token: connection.refreshToken, + email: connection.email, + }, + }); + const aliases = await driver.getEmailAliases(); + return NextResponse.json(aliases); + } catch (error) { + console.error('Error fetching email aliases:', error); + return NextResponse.json({ error: 'Failed to fetch email aliases' }, { status: 500 }); + } +} diff --git a/apps/mail/app/home/page.tsx b/apps/mail/app/home/page.tsx new file mode 100644 index 0000000000..980d008448 --- /dev/null +++ b/apps/mail/app/home/page.tsx @@ -0,0 +1,5 @@ +import HomeContent from '@/components/home/HomeContent'; + +export default function HomeRoute() { + return ; +} diff --git a/apps/mail/app/page.tsx b/apps/mail/app/page.tsx index 8aa81d10a7..e248ee50e5 100644 --- a/apps/mail/app/page.tsx +++ b/apps/mail/app/page.tsx @@ -1,44 +1,12 @@ -import { DemoMailLayout } from '@/components/mail/mail'; -import HeroImage from '@/components/home/hero-image'; -import { Skeleton } from '@/components/ui/skeleton'; -import Navbar from '@/components/home/navbar'; -import Footer from '@/components/home/footer'; -import Hero from '@/components/home/hero'; -import { Suspense } from 'react'; -import { createFeatureGate } from '@/lib/flags'; +import HomeContent from '@/components/home/HomeContent'; +import { getSession } from '@/lib/auth-client'; +import { redirect } from 'next/navigation'; export default async function Home() { - const isPrimaryLanding = await createFeatureGate("landing_title")(); - const title = isPrimaryLanding ?

- open source Gmail alternative -

:

- Gmail, but better. -

; + const session = await getSession(); + if (session.data?.user.id) { + redirect('/mail/inbox'); + } - return ( -
-
-
-
-
- }> - - - -
- }> - - -
-
- -
- {/**/} - {/* */} - {/*

CASA Certified

*/} - {/**/} -
-
-
- ); + return ; } diff --git a/apps/mail/components/connection/add.tsx b/apps/mail/components/connection/add.tsx index 6d509f88ab..be801d9419 100644 --- a/apps/mail/components/connection/add.tsx +++ b/apps/mail/components/connection/add.tsx @@ -32,10 +32,10 @@ export const AddConnectionDialog = ({ )} diff --git a/apps/mail/components/golden.tsx b/apps/mail/components/golden.tsx index 079250b5d5..f544eaa111 100644 --- a/apps/mail/components/golden.tsx +++ b/apps/mail/components/golden.tsx @@ -8,8 +8,9 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { Form, FormField, FormItem, FormLabel } from './ui/form'; -import { handleGoldenTicket } from '@/actions/settings'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useSidebar } from '@/components/ui/sidebar'; +import { useIsMobile } from '@/hooks/use-mobile'; import { useSession } from '@/lib/auth-client'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -17,11 +18,9 @@ import { useForm } from 'react-hook-form'; import { TicketIcon } from 'lucide-react'; import { Button } from './ui/button'; import { Input } from './ui/input'; +import Image from 'next/image'; import { toast } from 'sonner'; import { z } from 'zod'; -import { useSidebar } from '@/components/ui/sidebar'; -import { useIsMobile } from '@/hooks/use-mobile'; -import Image from 'next/image'; const schema = z.object({ email: z.string().email(), @@ -96,14 +95,28 @@ export const GoldenTicketModal = () => { - - Zero - Zero + + Zero + Zero Welcome to Zero! 🎉 ✨ - - Zero is still in early beta 🚀 and will continue to grow and improve from this point on. If - you know a friend who wants to test and try out Zero, send them an invite! 💌 + + + Zero is still in early beta 🚀 and will continue to grow and improve from this point + on. If you know a friend who wants to test and try out Zero, send them an invite! 💌 + You can only invite one person, so make it count! 🎯 ⭐️ @@ -115,12 +128,15 @@ export const GoldenTicketModal = () => { name="email" render={({ field }) => ( - - + )} /> -
+
diff --git a/apps/mail/components/home/HomeContent.tsx b/apps/mail/components/home/HomeContent.tsx new file mode 100644 index 0000000000..d89e3ffb5c --- /dev/null +++ b/apps/mail/components/home/HomeContent.tsx @@ -0,0 +1,48 @@ +import HeroImage from '@/components/home/hero-image'; +import { Skeleton } from '@/components/ui/skeleton'; +import { createFeatureGate } from '@/lib/flags'; +import Navbar from '@/components/home/navbar'; +import Footer from '@/components/home/footer'; +import { DemoMailLayout } from '../mail/mail'; +import Hero from '@/components/home/hero'; +import { Suspense } from 'react'; + +export default async function HomeContent() { + const isPrimaryLanding = await createFeatureGate('landing_title')(); + const title = isPrimaryLanding ? ( +

+ open source Gmail alternative +

+ ) : ( +

+ Gmail, but better. +

+ ); + + return ( +
+
+
+
+
+ }> + + + +
+ }> + + +
+
+ +
+ {/**/} + {/* */} + {/*

CASA Certified

*/} + {/**/} +
+
+
+ ); +} diff --git a/apps/mail/components/home/hero.tsx b/apps/mail/components/home/hero.tsx index 41392d8a49..25c83f57b6 100644 --- a/apps/mail/components/home/hero.tsx +++ b/apps/mail/components/home/hero.tsx @@ -2,11 +2,13 @@ import { Form, FormControl, FormField, FormItem } from '../ui/form'; import { AnimatedNumber } from '../ui/animated-number'; +import { useState, useEffect, ReactNode } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useSession } from '@/lib/auth-client'; import { Card, CardContent } from '../ui/card'; -import { useState, useEffect, ReactNode } from 'react'; import Balancer from 'react-wrap-balancer'; import { useForm } from 'react-hook-form'; +import { GithubIcon } from 'lucide-react'; import { GitHub } from '../icons/icons'; import confetti from 'canvas-confetti'; import { Button } from '../ui/button'; @@ -16,8 +18,6 @@ import { toast } from 'sonner'; import Link from 'next/link'; import axios from 'axios'; import { z } from 'zod'; -import { useSession } from '@/lib/auth-client'; -import { GithubIcon } from 'lucide-react'; const betaSignupSchema = z.object({ email: z.string().email().min(9), @@ -91,7 +91,7 @@ export default function Hero({ title }: { title: ReactNode }) { {title}
- Experience email the way you want with 0 – the first + Zero is an AI native email client that manages your email so you don't have to. The first open source email app that puts your privacy and safety first.
@@ -114,7 +114,7 @@ export default function Hero({ title }: { title: ReactNode }) { className="dark:hover:bg-accent flex h-[40px] w-[170px] items-center justify-center rounded-md bg-white text-gray-900 hover:bg-gray-100 hover:text-gray-900 dark:bg-black dark:text-white dark:hover:text-white" asChild > - + {' '} )} -
- +
{signupCount !== null && (
@@ -194,7 +193,14 @@ export default function Hero({ title }: { title: ReactNode }) {
)} -
{theme === 'dark' ? ( - + ) : ( - + )} -

{t('common.navUser.appTheme')}

+

{t('common.navUser.appTheme')}

- - + +
- -

{t('common.actions.settings')}

+ +

{t('common.actions.settings')}

- +
- -

{t('common.navUser.customerSupport')}

+ +

{t('common.navUser.customerSupport')}

- +
- -

{t('common.actions.logout')}

+ +

{t('common.actions.logout')}

@@ -294,10 +294,10 @@ export function NavUser() {

Debug

- +
- -

Clear Local Cache

+ +

Clear Local Cache

diff --git a/apps/mail/config/navigation.ts b/apps/mail/config/navigation.ts index f9f210740a..58478168be 100644 --- a/apps/mail/config/navigation.ts +++ b/apps/mail/config/navigation.ts @@ -172,7 +172,7 @@ export const navigationConfig: Record = { title: 'navigation.settings.shortcuts', url: '/settings/shortcuts', icon: KeyboardIcon, - // disabled: true, + disabled: true, }, // { // title: "Notifications", diff --git a/apps/mail/hooks/use-email-aliases.ts b/apps/mail/hooks/use-email-aliases.ts index 4ff2c46dc2..2b66d24f39 100644 --- a/apps/mail/hooks/use-email-aliases.ts +++ b/apps/mail/hooks/use-email-aliases.ts @@ -1,13 +1,18 @@ 'use client'; -import type { EmailAlias } from '../actions/email-aliases'; import useSWRImmutable from 'swr/immutable'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); +export type EmailAlias = { + email: string; + name?: string; + primary?: boolean; +}; + export function useEmailAliases() { const { data, error, isLoading, mutate } = useSWRImmutable( - '/api/driver/email-aliases', + '/api/v1/email-aliases', fetcher, ); diff --git a/apps/mail/hooks/use-stats.ts b/apps/mail/hooks/use-stats.ts index 931ac34737..96dc2d92c3 100644 --- a/apps/mail/hooks/use-stats.ts +++ b/apps/mail/hooks/use-stats.ts @@ -1,6 +1,5 @@ 'use client'; import { useSession } from '@/lib/auth-client'; -import { mailCount } from '@/actions/mail'; import axios from 'axios'; import useSWR from 'swr'; diff --git a/apps/mail/lib/auth.ts b/apps/mail/lib/auth.ts index 46dc71cc2f..6ff9ab98f0 100644 --- a/apps/mail/lib/auth.ts +++ b/apps/mail/lib/auth.ts @@ -165,7 +165,7 @@ const options = { console.log('Tried to add user to earlyAccess after error, failed', foundUser, err), ); try { - redirect('/login?error=early_access_required'); + throw redirect('/login?error=early_access_required'); } catch (error) { console.warn('Error redirecting to login page:', error); }