Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b9862fb
fix: local dev for zero backend
BlankParticle May 11, 2025
63f6619
Merge pull request #933 from Mail-0/fix/local-dev-server
MrgSub May 11, 2025
0c23150
refactor: restore headers logic and NotificationProvider in MailLayou…
MrgSub May 11, 2025
1e1a002
fix: use different cookie based on NODE_ENV
BlankParticle May 11, 2025
bee00f9
Merge pull request #938 from Mail-0/fix/seprate-cookie
MrgSub May 11, 2025
0abf236
refactor: simplify room assignment logic in NotificationProvider for …
MrgSub May 11, 2025
560ac66
Merge branch 'staging' of https://github.com/nizzyabi/mail into staging
MrgSub May 11, 2025
6c72d3b
fix: update room assignment logic in NotificationProvider to use conn…
MrgSub May 11, 2025
4526f25
fix(thread): attachment rendering
hiheyhello123 May 11, 2025
7df47bf
fix: update room assignment logic in NotificationProvider to use only…
MrgSub May 11, 2025
83d4ebf
Merge pull request #940 from Mail-0/fix/attachments
MrgSub May 11, 2025
812b78e
refactor: update defaultLabels structure in mail component for improv…
MrgSub May 11, 2025
a2df851
Merge branch 'staging' of https://github.com/nizzyabi/mail into staging
MrgSub May 11, 2025
d0e5d4f
feat: enhance NotificationProvider to handle message types and invali…
MrgSub May 11, 2025
fe7e088
feat: integrate useThreads hook and enhance message handling in Notif…
MrgSub May 11, 2025
ccb5783
fix: update logging in NotificationProvider to use console.warn for m…
MrgSub May 11, 2025
e3f70d1
refactor: simplify thread summary retrieval in brainRouter by using z…
MrgSub May 11, 2025
6033dee
fix: update AiSummary to correctly access summary data structure
MrgSub May 11, 2025
03b36a9
refactor: remove unused imports and add console warning for thread re…
MrgSub May 11, 2025
6ff91ef
refactor: improve layout and styling in PricingPage component for bet…
MrgSub May 11, 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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/zerodotemail"
BETTER_AUTH_SECRET=my-better-auth-secret
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
COOKIE_DOMAIN=localhost

COOKIE_DOMAIN="localhost"

# Change to your project's client ID and secret, these work with localhost:3000 and localhost:3001
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
Expand All @@ -27,6 +29,7 @@ AI_SYSTEM_PROMPT=""

GROQ_API_KEY=""

DEFAULT_BRAIN_LIMIT="500"
BRAIN_URL=""
NODE_ENV="development"

Expand Down
25 changes: 13 additions & 12 deletions apps/mail/app/(full-width)/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,12 @@ export default function PricingPage() {
<div className="container mx-auto mt-12 h-screen px-4 py-16 md:mt-24">
<div className="mb-12 text-center">
<h1 className="mb-2 text-4xl font-bold text-white md:text-6xl">Pricing</h1>
<p className="text-white/50 text-lg">Choose the plan that's right for you</p>
<p className="text-lg text-white/50">Choose the plan that's right for you</p>
</div>

<div className="mx-auto max-w-5xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Free Plan */}
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<div className="relative flex h-full flex-col rounded-xl border bg-[#121212] px-8 pb-4 pt-8">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Free</h1>
<div className="mb-4 text-center text-3xl font-bold dark:text-white">
$0 <span className="text-lg font-medium">/ mo</span>
Expand All @@ -237,18 +237,22 @@ export default function PricingPage() {
</li>

<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI chat <span className="text-white/50 text-xs">(25 per day)</span>
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI chat{' '}
<span className="text-xs text-white/50">(25 per day)</span>
</li>

<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI writing assistant
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Auto labeling<span className="text-white/50 text-xs">(25 per day)</span>
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Auto labeling
<span className="text-xs text-white/50">(25 per day)</span>
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" />
<span>AI thread summaries <span className="text-white/50 text-xs">(25 per day)</span></span>
<span>
AI thread summaries <span className="text-xs text-white/50">(25 per day)</span>
</span>
</li>
{/* <li className="flex items-center gap-2">
<CircleX className="h-4 w-4 fill-white opacity-50" /> Instant thread AI-generated
Expand All @@ -263,14 +267,12 @@ export default function PricingPage() {
</li> */}
</ul>
<Link href="/login">
<Button className="h-8 w-full" onClick={handleUpgrade}>
Get Started
</Button>
<Button className="h-8 w-full">Get Started</Button>
</Link>
</div>

{/* Pro Plan */}
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<div className="relative flex h-full flex-col rounded-xl border bg-[#121212] px-8 pb-4 pt-8">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Pro</h1>

<div className="mb-4 text-center text-3xl font-bold dark:text-white">
Expand Down Expand Up @@ -298,15 +300,14 @@ export default function PricingPage() {
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Verified checkmark
</li>

</ul>
<Button className="h-8 w-full" onClick={handleUpgrade}>
Get Started
</Button>
</div>

{/* Enterprise Plan */}
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<div className="relative flex h-full flex-col rounded-xl border bg-[#121212] px-8 pb-4 pt-8">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Enterprise</h1>

<div className="mb-4 text-center text-3xl font-bold dark:text-white">Contact us</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/mail/app/(routes)/mail/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { NotificationProvider } from '@/components/party';
import { AppSidebar } from '@/components/ui/app-sidebar';
import { headers } from 'next/headers';
export default async function MailLayout({ children }: { children: React.ReactNode }) {
// const headersList = await headers();
const headersList = await headers();
return (
<HotkeyProviderWrapper>
<AppSidebar />
<div className="bg-lightBackground dark:bg-darkBackground w-full">{children}</div>
<OnboardingWrapper />
{/* <NotificationProvider headers={Object.fromEntries(headersList.entries())} /> */}
<NotificationProvider headers={Object.fromEntries(headersList.entries())} />
</HotkeyProviderWrapper>
);
}
4 changes: 2 additions & 2 deletions apps/mail/components/mail/mail-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ const AiSummary = () => {
};

if (isLoading) return null;
if (!summary?.short?.length) return null;
if (!summary?.data.short?.length) return null;

return (
<div
Expand All @@ -267,7 +267,7 @@ const AiSummary = () => {
)}
</div>
{showSummary && (
<Markdown markdownContainerStyles={{ fontSize: 12 }}>{summary?.short || ''}</Markdown>
<Markdown markdownContainerStyles={{ fontSize: 12 }}>{summary?.data.short || ''}</Markdown>
)}
</div>
);
Expand Down
51 changes: 34 additions & 17 deletions apps/mail/components/mail/mail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,37 @@ interface Tag {
text: string;
}

const defaultLabels = [
'urgent',
'review',
'followup',
'decision',
'work',
'finance',
'legal',
'hiring',
'sales',
'product',
'support',
'vendors',
'marketing',
'meeting',
'investors',
export const defaultLabels = [
{
name: 'to respond',
usecase: 'emails you need to respond to. NOT sales, marketing, or promotions.',
},
{
name: 'FYI',
usecase:
'emails that are not important, but you should know about. NOT sales, marketing, or promotions.',
},
{
name: 'comment',
usecase:
'Team chats in tools like Google Docs, Slack, etc. NOT marketing, sales, or promotions.',
},
{
name: 'notification',
usecase: 'Automated updates from services you use. NOT sales, marketing, or promotions.',
},
{
name: 'promotion',
usecase: 'Sales, marketing, cold emails, special offers or promotions. NOT to respond to.',
},
{
name: 'meeting',
usecase: 'Calendar events, invites, etc. NOT sales, marketing, or promotions.',
},
{
name: 'billing',
usecase: 'Billing notifications. NOT sales, marketing, or promotions.',
},
];

const AutoLabelingSettings = () => {
Expand All @@ -101,7 +116,9 @@ const AutoLabelingSettings = () => {
}, [storedLabels]);

const handleResetToDefault = useCallback(() => {
setLabels(defaultLabels.map((label) => ({ id: label, name: label, text: label })));
setLabels(
defaultLabels.map((label) => ({ id: label.name, name: label.name, text: label.name })),
);
}, [storedLabels]);

return (
Expand Down
27 changes: 18 additions & 9 deletions apps/mail/components/party.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
'use client';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { usePartySocket } from 'partysocket/react';
import { useThreads } from '@/hooks/use-threads';
import { useSession } from '@/lib/auth-client';
import { useEffect } from 'react';

export const NotificationProvider = ({ headers }: { headers: Record<string, string> }) => {
// const trpc = useTRPC();
const trpc = useTRPC();
const { data: session } = useSession();

const queryClient = useQueryClient();
const [{ refetch: refetchThreads }] = useThreads();
usePartySocket({
party: 'durable-mailbox',
room: session?.activeConnection?.email
? `${session.activeConnection.email}:general`
: 'general',
room: session?.activeConnection?.id ? `${session.activeConnection.id}` : 'general',
prefix: 'zero',
debug: true,
maxRetries: 1,
query: {
token: headers['cookie'],
},
host: process.env.NEXT_PUBLIC_BACKEND_URL!,
onMessage: (message) => {
console.log(message);
onMessage: async (message: MessageEvent<string>) => {
console.warn('party message', message);
const [threadId, type] = message.data.split(':');
if (type === 'end') {
console.log('invalidating thread', threadId);
await queryClient.invalidateQueries({
queryKey: trpc.mail.get.queryKey({ id: threadId }),
});
await refetchThreads();
console.warn('refetched threads');
}
console.warn('party message', threadId, type);
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "wrangler dev --show-interactive-dev-session=false --env staging",
"build": "wrangler deploy --minify --dry-run",
"deploy": "wrangler deploy --minify",
"types": "wrangler types --e staging"
"types": "wrangler types --env staging"
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

Fixed incorrect flag in wrangler command

Corrected the flag from --e staging to --env staging in the types script. This ensures the proper environment is specified when generating types.

},
"exports": {
"./trpc": "./src/trpc/index.ts",
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
session,
userHotkeys,
} from '@zero/db/schema';
import { type Account, betterAuth, type BetterAuthOptions } from 'better-auth';
import { createAuthMiddleware, customSession } from 'better-auth/plugins';
import { defaultUserSettings } from '@zero/db/user_settings_default';
import { getBrowserTimezone, isValidTimezone } from './timezones';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { type Account, betterAuth } from 'better-auth';
import { getSocialProviders } from './auth-providers';
import { getActiveDriver } from './driver/utils';
import { APIError } from 'better-auth/api';
Expand Down Expand Up @@ -205,6 +205,7 @@ const createAuthConfig = () => {
ipAddress: {
disableIpTracking: true,
},
cookiePrefix: env.NODE_ENV === 'development' ? 'better-auth-dev' : 'better-auth',
crossSubDomainCookies: {
enabled: true,
domain: env.COOKIE_DOMAIN,
Expand Down Expand Up @@ -320,7 +321,7 @@ const createAuthConfig = () => {
};
}),
],
};
} satisfies BetterAuthOptions;
};

export const createSimpleAuth = () => {
Expand Down
91 changes: 56 additions & 35 deletions apps/server/src/lib/driver/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,45 +293,34 @@ export class GoogleMailManager implements MailManager {
});
}

const attachmentParts = message.payload?.parts
? this.findAttachments(message.payload.parts)
: [];

const attachments = await Promise.all(
message.payload?.parts
?.filter((part) => {
if (!part.filename || part.filename.length === 0) return false;

const contentDisposition =
part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')
?.value || '';
const isInline = contentDisposition.toLowerCase().includes('inline');

const hasContentId = part.headers?.some(
(h) => h.name?.toLowerCase() === 'content-id',
);

return !isInline || (isInline && !hasContentId);
})
?.map(async (part) => {
const attachmentId = part.body?.attachmentId;
if (!attachmentId) {
return null;
}
attachmentParts.map(async (part) => {
const attachmentId = part.body?.attachmentId;
if (!attachmentId) {
return null;
}

try {
if (!message.id) {
return null;
}
const attachmentData = await this.getAttachment(message.id, attachmentId);
return {
filename: part.filename || '',
mimeType: part.mimeType || '',
size: Number(part.body?.size || 0),
attachmentId: attachmentId,
headers: part.headers || [],
body: attachmentData ?? '',
};
} catch {
try {
if (!message.id) {
return null;
}
}) || [],
const attachmentData = await this.getAttachment(message.id, attachmentId);
return {
filename: part.filename || '',
mimeType: part.mimeType || '',
size: Number(part.body?.size || 0),
attachmentId: attachmentId,
headers: part.headers || [],
body: attachmentData ?? '',
};
} catch {
return null;
}
})
).then((attachments) =>
attachments.filter((a): a is NonNullable<typeof a> => a !== null),
);
Expand Down Expand Up @@ -1044,4 +1033,36 @@ export class GoogleMailManager implements MailManager {
throw new StandardizedError(error, operation, context);
}
}

private findAttachments(parts: any[]): any[] {
let results: any[] = [];

for (const part of parts) {
if (part.filename && part.filename.length > 0) {
const contentDisposition =
part.headers?.find((h: any) => h.name?.toLowerCase() === 'content-disposition')
?.value || '';
const isInline = contentDisposition.toLowerCase().includes('inline');
const hasContentId = part.headers?.some(
(h: any) => h.name?.toLowerCase() === 'content-id',
);

if (!isInline || (isInline && !hasContentId)) {
results.push(part);
}
}

if (part.parts && Array.isArray(part.parts)) {
results = results.concat(this.findAttachments(part.parts));
}

if (part.body?.attachmentId && part.mimeType === 'message/rfc822') {
if (part.filename && part.filename.length > 0) {
results.push(part);
}
}
}

return results;
}
}
Loading