+
+ setOpen(false)}>
+ Home
+
Pricing
diff --git a/apps/mail/components/party.tsx b/apps/mail/components/party.tsx
index 66a4205939..36f7d665f5 100644
--- a/apps/mail/components/party.tsx
+++ b/apps/mail/components/party.tsx
@@ -2,28 +2,15 @@ import { useActiveConnection } from '@/hooks/use-connections';
import { useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { usePartySocket } from 'partysocket/react';
-import { useThreads } from '@/hooks/use-threads';
-import { useLabels } from '@/hooks/use-labels';
-import { useSession } from '@/lib/auth-client';
import { funnel } from 'remeda';
const DEBOUNCE_DELAY = 10_000; // 10 seconds is appropriate for real-time notifications
export const NotificationProvider = () => {
const trpc = useTRPC();
- // const { refetch: refetchLabels } = useLabels();
const queryClient = useQueryClient();
- // const [{ refetch: refetchThreads }] = useThreads();
const { data: activeConnection } = useActiveConnection();
- // const handleRefetchLabels = useCallback(async () => {
- // await refetchLabels();
- // }, [refetchLabels]);
-
- // const handleRefetchThreads = useCallback(async () => {
- // await refetchThreads();
- // }, [refetchThreads]);
-
const labelsDebouncer = funnel(
() => queryClient.invalidateQueries({ queryKey: trpc.labels.list.queryKey() }),
{ minQuietPeriodMs: DEBOUNCE_DELAY },
diff --git a/apps/mail/components/ui/nav-main.tsx b/apps/mail/components/ui/nav-main.tsx
index bd23155b58..c50a58a333 100644
--- a/apps/mail/components/ui/nav-main.tsx
+++ b/apps/mail/components/ui/nav-main.tsx
@@ -148,10 +148,7 @@ export function NavMain({ items }: NavMainProps) {
[pathname, category, searchParams, isValidInternalUrl],
);
- const activeAccount = React.useMemo(() => {
- if (!activeConnection?.id || !connections?.connections) return null;
- return connections.connections.find((connection) => connection.id === activeConnection?.id);
- }, [activeConnection?.id, connections?.connections]);
+ const { data: activeAccount } = useActiveConnection();
const isUrlActive = useCallback(
(url: string) => {
@@ -263,11 +260,9 @@ export function NavMain({ items }: NavMainProps) {
) : activeAccount?.providerId === 'microsoft' ? null : null}
-
+ {activeAccount ? (
+
+ ) : null}
)}
diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx
index 325e5d4c29..165b1a2c40 100644
--- a/apps/mail/components/ui/nav-user.tsx
+++ b/apps/mail/components/ui/nav-user.tsx
@@ -26,6 +26,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { CallInboxDialog, SetupInboxDialog } from '../setup-phone';
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useLoading } from '../context/loading-context';
import { signOut, useSession } from '@/lib/auth-client';
import { AddConnectionDialog } from '../connection/add';
import { useTRPC } from '@/providers/query-provider';
@@ -59,9 +60,9 @@ export function NavUser() {
const [searchParams] = useSearchParams();
const queryClient = useQueryClient();
const { data: activeConnection, refetch: refetchActiveConnection } = useActiveConnection();
- const { revalidate } = useRevalidator();
const [, setPricingDialog] = useQueryState('pricingDialog');
const [category] = useQueryState('category', { defaultValue: 'All Mail' });
+ const { setLoading } = useLoading();
const getSettingsHref = useCallback(() => {
const currentPath = category
@@ -81,21 +82,69 @@ export function NavUser() {
toast.success('Connection ID copied to clipboard');
}, [activeConnection]);
- const activeAccount = useMemo(() => {
- if (!activeConnection || !data) return null;
- return data.connections?.find((connection) => connection.id === activeConnection.id);
- }, [activeConnection, data]);
+ const { data: activeAccount } = useActiveConnection();
useEffect(() => setIsRendered(true), []);
const handleAccountSwitch = (connectionId: string) => async () => {
if (connectionId === activeConnection?.id) return;
- setThreadId(null);
- await setDefaultConnection({ connectionId });
- await refetchActiveConnection();
- await refetchConnections();
- await revalidate();
- refetchSession();
+
+ try {
+ setLoading(true, t('common.navUser.switchingAccounts'));
+
+ setThreadId(null);
+
+ await setDefaultConnection({ connectionId });
+
+ const targetConnection = data?.connections?.find((conn: any) => conn.id === connectionId);
+ if (targetConnection) {
+ queryClient.setQueryData(trpc.connections.getDefault.queryKey(), targetConnection);
+ }
+
+ await Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: trpc.connections.getDefault.queryKey(),
+ }),
+ queryClient.invalidateQueries({
+ queryKey: trpc.connections.list.queryKey(),
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['mail']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['labels']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['stats']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['notes']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['brain']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['settings']],
+ }),
+
+ queryClient.removeQueries({
+ queryKey: [['drafts']],
+ }),
+ ]);
+ } catch (error) {
+ console.error('Error switching accounts:', error);
+ toast.error(t('common.navUser.failedToSwitchAccount'));
+
+ await refetchActiveConnection();
+ } finally {
+ setLoading(false);
+ }
};
const handleLogout = async () => {
diff --git a/apps/mail/components/ui/prompts-dialog.tsx b/apps/mail/components/ui/prompts-dialog.tsx
index d10bce2505..319b1794f5 100644
--- a/apps/mail/components/ui/prompts-dialog.tsx
+++ b/apps/mail/components/ui/prompts-dialog.tsx
@@ -1,3 +1,16 @@
+import {
+ BookDashedIcon,
+ GitBranchPlus,
+ MessageSquareIcon,
+ RefreshCcwDotIcon,
+ SendIcon,
+ RotateCcwIcon,
+} from 'lucide-react';
+import {
+ SummarizeMessage,
+ SummarizeThread,
+ ReSummarizeThread,
+} from '../../../server/src/lib/brain.fallback.prompts';
import {
Dialog,
DialogContent,
@@ -6,26 +19,106 @@ import {
DialogTitle,
DialogTrigger,
} from './dialog';
-import {
- BookDashedIcon,
- GitBranchPlus,
- MessageSquareIcon,
- RefreshCcwDotIcon,
- SendIcon,
-} from 'lucide-react';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
import { AiChatPrompt, StyledEmailAssistantSystemPrompt } from '@/lib/prompts';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
+import { EPrompts } from '../../../server/src/types';
+import { useMutation } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
+import { useForm } from 'react-hook-form';
import { Paper } from '../icons/icons';
import { Textarea } from './textarea';
import { Link } from 'react-router';
+import { useMemo } from 'react';
+import { toast } from 'sonner';
+
+const isPromptValid = (prompt: string): boolean => {
+ const trimmed = prompt.trim();
+ return trimmed !== '' && trimmed.toLowerCase() !== 'undefined';
+};
+
+const initialValues: Record
= {
+ [EPrompts.Chat]: '',
+ [EPrompts.Compose]: '',
+ [EPrompts.SummarizeThread]: '',
+ [EPrompts.ReSummarizeThread]: '',
+ [EPrompts.SummarizeMessage]: '',
+};
+
+const fallbackPrompts = {
+ [EPrompts.Chat]: AiChatPrompt('', '', ''),
+ [EPrompts.Compose]: StyledEmailAssistantSystemPrompt(),
+ [EPrompts.SummarizeThread]: SummarizeThread,
+ [EPrompts.ReSummarizeThread]: ReSummarizeThread,
+ [EPrompts.SummarizeMessage]: SummarizeMessage,
+};
export function PromptsDialog() {
const trpc = useTRPC();
+ const queryClient = useQueryClient();
const { data: prompts } = useQuery(trpc.brain.getPrompts.queryOptions());
+
+ const { mutateAsync: updatePrompt, isPending: isSavingPrompt } = useMutation(
+ trpc.brain.updatePrompt.mutationOptions({
+ onSuccess: () => {
+ toast.success('Prompt updated');
+ queryClient.invalidateQueries({ queryKey: trpc.brain.getPrompts.queryKey() });
+ },
+ onError: (error) => {
+ toast.error(error.message ?? 'Failed to update prompt');
+ },
+ }),
+ );
+
+ const mappedValues = useMemo(() => {
+ if (!prompts) return initialValues;
+ return Object.fromEntries(
+ Object.entries(initialValues).map(([key]) => [
+ key,
+ isPromptValid(prompts[key as EPrompts] ?? '')
+ ? prompts[key as EPrompts]
+ : fallbackPrompts[key as EPrompts],
+ ]),
+ ) as Record;
+ }, [prompts]);
+
+ const { register, getValues, setValue } = useForm>({
+ defaultValues: initialValues,
+ values: mappedValues,
+ });
+
+ const resetToDefault = (promptType: EPrompts) => {
+ setValue(promptType, fallbackPrompts[promptType]);
+ };
+
+ const renderPromptButtons = (promptType: EPrompts, enumType: EPrompts) => (
+
+
+ updatePrompt({
+ promptType: enumType,
+ content: getValues(promptType),
+ })
+ }
+ disabled={isSavingPrompt}
+ >
+ Save
+
+ resetToDefault(promptType)}
+ disabled={isSavingPrompt}
+ >
+
+ Reset to Default
+
+
+ );
+
return (
@@ -53,8 +146,7 @@ export function PromptsDialog() {
- We believe in Open Source, so we're open sourcing our AI system prompts. Soon you will
- be able to customize them to your liking.
+ We believe in Open Source, so we're open sourcing our AI system prompts.
@@ -75,48 +167,53 @@ export function PromptsDialog() {
Summarize Message
-
-
- This system prompt is used in the chat sidebar agent. The agent has multiple tools
- available.
-
-
-
-
-
- This system prompt is used to compose emails that sound like you.
-
-
-
{prompts ? (
-
+
+
+ This system prompt is used in the chat sidebar agent. The agent has multiple tools
+ available.
+
+
+ {renderPromptButtons(EPrompts.Chat, EPrompts.Chat)}
+
+ ) : null}
+ {prompts ? (
+
+
+ This system prompt is used to compose emails that sound like you.
+
+
+ {renderPromptButtons(EPrompts.Compose, EPrompts.Compose)}
+
+ ) : null}
+ {prompts ? (
+
This system prompt is used to summarize threads. It takes the entire thread and
key information and summarizes them.
-
+
+ {renderPromptButtons(EPrompts.SummarizeThread, EPrompts.SummarizeThread)}
) : null}
{prompts ? (
-
+
This system prompt is used to re-summarize threads. It's used when the thread
messages change and a new context is needed.
-
+
+ {renderPromptButtons(EPrompts.ReSummarizeThread, EPrompts.ReSummarizeThread)}
) : null}
{prompts ? (
-
+
This system prompt is used to summarize messages. It takes a single message and
summarizes it.
-
+
+ {renderPromptButtons(EPrompts.SummarizeMessage, EPrompts.SummarizeMessage)}
) : null}
@@ -124,4 +221,4 @@ export function PromptsDialog() {
);
-}
+}
\ No newline at end of file
diff --git a/apps/mail/components/ui/sidebar-labels.tsx b/apps/mail/components/ui/sidebar-labels.tsx
index b15d1c3a6c..454089090e 100644
--- a/apps/mail/components/ui/sidebar-labels.tsx
+++ b/apps/mail/components/ui/sidebar-labels.tsx
@@ -1,21 +1,11 @@
+import type { IConnection, Label as LabelType } from '@/types';
import { RecursiveFolder } from './recursive-folder';
-import type { Label as LabelType } from '@/types';
import { Tree } from '../magicui/file-tree';
import { useCallback } from 'react';
type Props = {
data: LabelType[];
- activeAccount:
- | {
- id: string;
- email: string;
- name: string | null;
- picture: string | null;
- createdAt: Date;
- providerId: 'google' | 'microsoft';
- }
- | null
- | undefined;
+ activeAccount: IConnection | null | undefined;
stats:
| {
count?: number;
diff --git a/apps/mail/hooks/use-categories.ts b/apps/mail/hooks/use-categories.ts
index 5a2de8e502..f9972c0517 100644
--- a/apps/mail/hooks/use-categories.ts
+++ b/apps/mail/hooks/use-categories.ts
@@ -8,7 +8,8 @@ export interface CategorySetting {
name: string;
searchValue: string;
order: number;
- isDefault?: boolean;
+ icon?: string;
+ isDefault: boolean;
}
export function useCategorySettings(): CategorySetting[] {
diff --git a/apps/mail/hooks/use-compose-editor.ts b/apps/mail/hooks/use-compose-editor.ts
index 4314615d87..71d0c3b79a 100644
--- a/apps/mail/hooks/use-compose-editor.ts
+++ b/apps/mail/hooks/use-compose-editor.ts
@@ -1,9 +1,11 @@
import { useEditor, type KeyboardShortcutCommand, Extension, generateJSON } from '@tiptap/react';
import { AutoComplete } from '@/components/create/editor-autocomplete';
import { defaultExtensions } from '@/components/create/extensions';
+import { FileHandler } from '@tiptap/extension-file-handler';
import Placeholder from '@tiptap/extension-placeholder';
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { TextSelection } from 'prosemirror-state';
+import { Image } from '@tiptap/extension-image';
import { Markdown } from 'tiptap-markdown';
import { isObjectType } from 'remeda';
import { cn } from '@/lib/utils';
@@ -187,6 +189,53 @@ const useComposeEditor = ({
const extensions = [
...defaultExtensions,
Markdown,
+ Image,
+ FileHandler.configure({
+ allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
+ onDrop: (currentEditor, files, pos) => {
+ files.forEach((file) => {
+ const fileReader = new FileReader();
+
+ fileReader.readAsDataURL(file);
+ fileReader.onload = () => {
+ currentEditor
+ .chain()
+ .insertContentAt(pos, {
+ type: 'image',
+ attrs: {
+ src: fileReader.result,
+ },
+ })
+ .focus()
+ .run();
+ };
+ });
+ },
+ onPaste: (currentEditor, files, htmlContent) => {
+ files.forEach((file) => {
+ if (htmlContent) {
+ console.log(htmlContent); // eslint-disable-line no-console
+ return false;
+ }
+
+ const fileReader = new FileReader();
+
+ fileReader.readAsDataURL(file);
+ fileReader.onload = () => {
+ currentEditor
+ .chain()
+ .insertContentAt(currentEditor.state.selection.anchor, {
+ type: 'image',
+ attrs: {
+ src: fileReader.result,
+ },
+ })
+ .focus()
+ .run();
+ };
+ });
+ },
+ }),
AutoCompleteExtension({
myInfo,
sender,
@@ -209,13 +258,14 @@ const useComposeEditor = ({
Placeholder.configure({
placeholder,
}),
- ...(onAttachmentsChange
- ? [
- PreventNavigateOnDragOver((files) => {
- onAttachmentsChange(files);
- }),
- ]
- : []),
+ // breaks the image upload
+ // ...(onAttachmentsChange
+ // ? [
+ // PreventNavigateOnDragOver((files) => {
+ // onAttachmentsChange(files);
+ // }),
+ // ]
+ // : []),
];
return useEditor({
diff --git a/apps/mail/hooks/use-mail-navigation.ts b/apps/mail/hooks/use-mail-navigation.ts
index a803f1eae1..f7c18f3d0b 100644
--- a/apps/mail/hooks/use-mail-navigation.ts
+++ b/apps/mail/hooks/use-mail-navigation.ts
@@ -1,3 +1,4 @@
+import { useCommandPalette } from '@/components/context/command-palette-context';
import { useCallback, useEffect, useState, useRef } from 'react';
import { useOptimisticActions } from './use-optimistic-actions';
import { useMail } from '@/components/mail/use-mail';
@@ -22,6 +23,7 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
itemsRef.current = items;
const onNavigateRef = useRef(onNavigate);
onNavigateRef.current = onNavigate;
+ const { open: isCommandPaletteOpen } = useCommandPalette();
const hoveredMailRef = useRef(null);
const keyboardActiveRef = useRef(false);
@@ -193,12 +195,12 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
keyboardActiveRef.current = false;
}, [setFocusedIndex, onNavigateRef]);
- useHotkeys('ArrowUp', handleArrowUp, { preventDefault: true });
- useHotkeys('ArrowDown', handleArrowDown, { preventDefault: true });
- useHotkeys('j', handleArrowDown);
- useHotkeys('k', handleArrowUp);
- useHotkeys('Enter', handleEnter, { preventDefault: true });
- useHotkeys('Escape', handleEscape, { preventDefault: true });
+ useHotkeys('ArrowUp', handleArrowUp, { preventDefault: true, enabled: !isCommandPaletteOpen });
+ useHotkeys('ArrowDown', handleArrowDown, { preventDefault: true, enabled: !isCommandPaletteOpen });
+ useHotkeys('j', handleArrowDown,{enabled: !isCommandPaletteOpen });
+ useHotkeys('k', handleArrowUp, { enabled: !isCommandPaletteOpen });
+ useHotkeys('Enter', handleEnter, { preventDefault: true,enabled: !isCommandPaletteOpen });
+ useHotkeys('Escape', handleEscape, { preventDefault: true,enabled: !isCommandPaletteOpen });
const handleMouseEnter = useCallback(
(threadId: string) => {
@@ -239,6 +241,7 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
const MOVE_DELAY = 100;
const handleKeyDown = (event: KeyboardEvent) => {
+ if (isCommandPaletteOpen) return;
if (!event.repeat) return;
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
@@ -266,7 +269,13 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
- }, [fastScroll]);
+ }, [fastScroll, isCommandPaletteOpen]);
+
+ useEffect(() => {
+ if (isCommandPaletteOpen) {
+ keyboardActiveRef.current = false;
+ }
+ }, [isCommandPaletteOpen]);
return {
focusedIndex,
diff --git a/apps/mail/lib/email-utils.client.tsx b/apps/mail/lib/email-utils.client.tsx
index c33cb71c92..2362fe8ceb 100644
--- a/apps/mail/lib/email-utils.client.tsx
+++ b/apps/mail/lib/email-utils.client.tsx
@@ -126,10 +126,6 @@ const proxyImageUrls = (html: string): string => {
if (proxiedUrl !== src) {
img.setAttribute('data-original-src', src);
img.setAttribute('src', proxiedUrl);
- img.setAttribute(
- 'onerror',
- `this.onerror=null; this.src=this.getAttribute('data-original-src');`,
- );
}
});
diff --git a/apps/mail/lib/hotkeys/global-hotkeys.tsx b/apps/mail/lib/hotkeys/global-hotkeys.tsx
index 01dd123d97..13e99392ef 100644
--- a/apps/mail/lib/hotkeys/global-hotkeys.tsx
+++ b/apps/mail/lib/hotkeys/global-hotkeys.tsx
@@ -7,13 +7,14 @@ import { useQueryState } from 'nuqs';
export function GlobalHotkeys() {
const [composeOpen, setComposeOpen] = useQueryState('isComposeOpen');
- const { openModal, clearAllFilters } = useCommandPalette();
+ const { clearAllFilters } = useCommandPalette();
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useQueryState('isCommandPaletteOpen');
const { undoLastAction } = useOptimisticActions();
const scope = 'global';
const handlers = {
newEmail: () => setComposeOpen('true'),
- commandPalette: () => openModal(),
+ commandPalette: () => setIsCommandPaletteOpen('true'),
clearAllFilters: () => clearAllFilters(),
undoLastAction: () => {
undoLastAction();
diff --git a/apps/mail/lib/notes-utils.ts b/apps/mail/lib/notes-utils.ts
index adfe1a217d..a426935d26 100644
--- a/apps/mail/lib/notes-utils.ts
+++ b/apps/mail/lib/notes-utils.ts
@@ -1,5 +1,6 @@
import type { Note } from '@/types';
import React from 'react';
+import { formatDate } from './utils';
export const NOTE_COLORS = [
{ value: 'default', label: 'Default', class: 'border-transparent', bgClass: '', style: {} },
@@ -107,28 +108,10 @@ export function formatRelativeTime(dateInput: string | Date, formatter?: any): s
const days = Math.floor(diffInSeconds / 86400);
return `${days} ${days === 1 ? 'day' : 'days'} ago`;
} else {
- return formatDate(date);
+ return formatDate(date, formatter);
}
}
-export function formatDate(dateInput: string | Date, formatter?: any): string {
- const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
-
- if (formatter) {
- return formatter.dateTime(date, {
- dateStyle: 'medium',
- timeStyle: 'short',
- });
- }
-
- return date.toLocaleString(undefined, {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- });
-}
export function sortNotes(notes: Note[]): Note[] {
const pinnedNotes = notes.filter((note) => note.isPinned);
diff --git a/apps/mail/lib/sanitize-tip-tap-html.tsx b/apps/mail/lib/sanitize-tip-tap-html.tsx
index c52d02354f..e8b3dfa9b3 100644
--- a/apps/mail/lib/sanitize-tip-tap-html.tsx
+++ b/apps/mail/lib/sanitize-tip-tap-html.tsx
@@ -3,7 +3,14 @@ import { Html } from '@react-email/components';
import sanitizeHtml from 'sanitize-html';
export const sanitizeTipTapHtml = async (html: string) => {
- const clean = sanitizeHtml(html);
+ const clean = sanitizeHtml(html, {
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+ allowedAttributes: {
+ ...sanitizeHtml.defaults.allowedAttributes,
+ img: ['src', 'alt', 'width', 'height', 'style'],
+ },
+ allowedSchemes: ['http', 'https', 'cid', 'data'],
+ });
return renderToString(
diff --git a/apps/mail/lib/schemas.ts b/apps/mail/lib/schemas.ts
index aab5a30fbe..d92fc4445d 100644
--- a/apps/mail/lib/schemas.ts
+++ b/apps/mail/lib/schemas.ts
@@ -51,7 +51,7 @@ export const createDraftData = z.object({
bcc: z.string().optional(),
subject: z.string(),
message: z.string(),
- attachments: z.array(serializedFileSchema).transform(deserializeFiles).optional(),
+ attachments: z.array(serializedFileSchema).optional(),
id: z.string().nullable(),
});
diff --git a/apps/mail/lib/utils.ts b/apps/mail/lib/utils.ts
index 7961024c58..12c2222658 100644
--- a/apps/mail/lib/utils.ts
+++ b/apps/mail/lib/utils.ts
@@ -66,24 +66,89 @@ export const getCookie = (key: string): string | null => {
return cookies?.[key] ?? null;
};
-export const formatDate = (date: string) => {
+export const parseAndValidateDate = (dateString: string): Date | null => {
try {
- // Handle empty or invalid input
- if (!date) {
- return '';
+ // Handle empty input
+ if (!dateString) {
+ return null;
}
- const timezone = getBrowserTimezone();
// Parse the date string to a Date object
- const dateObj = new Date(date);
- const now = new Date();
+ const dateObj = new Date(dateString);
// Check if the date is valid
if (isNaN(dateObj.getTime())) {
- console.error('Invalid date', date);
- return '';
+ console.error('Invalid date', dateString);
+ return null;
+ }
+
+ return dateObj;
+ } catch (error) {
+ console.error('Error parsing date', error);
+ return null;
+ }
+};
+
+/**
+ * Helper function to determine if a separate time display is needed
+ * Returns false for emails from today or within last 12 hours since formatDate already shows time for these
+ */
+export const shouldShowSeparateTime = (dateString: string | undefined): boolean => {
+ if (!dateString) return false;
+
+ const dateObj = parseAndValidateDate(dateString);
+ if (!dateObj) return false;
+
+ const now = new Date();
+
+ // Don't show separate time if email is from today
+ if (isToday(dateObj)) return false;
+
+ // Don't show separate time if email is within the last 12 hours
+ const hoursDifference = (now.getTime() - dateObj.getTime()) / (1000 * 60 * 60);
+ if (hoursDifference <= 12) return false;
+
+ // Show separate time for older emails
+ return true;
+};
+
+/**
+ * Formats a date with different formatting logic based on parameters
+ * Overloaded to handle both mail date formatting and notes date formatting
+ */
+export function formatDate(date: string): string;
+export function formatDate(dateInput: string | Date, formatter?: any): string;
+export function formatDate(dateInput: string | Date, formatter?: any): string {
+ // Notes formatting logic (when formatter is provided or date is a Date object)
+ if (formatter || dateInput instanceof Date) {
+ const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
+
+ if (formatter) {
+ return formatter.dateTime(date, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ });
}
+ return date.toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }
+
+ // Original mail formatting logic
+ const dateObj = parseAndValidateDate(dateInput as string);
+ if (!dateObj) {
+ return '';
+ }
+
+ try {
+ const timezone = getBrowserTimezone();
+ const now = new Date();
+
// If it's today, always show the time
if (isToday(dateObj)) {
return formatInTimeZone(dateObj, timezone, 'h:mm a');
@@ -110,6 +175,23 @@ export const formatDate = (date: string) => {
}
};
+export const formatTime = (date: string) => {
+ const dateObj = parseAndValidateDate(date);
+ if (!dateObj) {
+ return '';
+ }
+
+ try {
+ const timezone = getBrowserTimezone();
+
+ // Always return the time in h:mm a format
+ return formatInTimeZone(dateObj, timezone, 'h:mm a');
+ } catch (error) {
+ console.error('Error formatting time', error);
+ return '';
+ }
+};
+
export const cleanEmailAddress = (email: string = '') => {
return email.replace(/[<>]/g, '').trim();
};
diff --git a/apps/mail/locales/ar.json b/apps/mail/locales/ar.json
index 1924be9d58..8685bd33e7 100644
--- a/apps/mail/locales/ar.json
+++ b/apps/mail/locales/ar.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "المجتمع",
"documentation": "الوثائق",
- "appTheme": "مظهر التطبيق",
+ "appTheme": "تبديل السمة",
"accounts": "الحسابات",
"signIn": "تسجيل الدخول",
- "otherAccounts": "حسابات أخرى"
+ "otherAccounts": "حسابات أخرى",
+ "switchingAccounts": "جارٍ تبديل الحسابات...",
+ "accountSwitched": "تم تبديل الحساب بنجاح",
+ "failedToSwitchAccount": "فشل تبديل الحسابات. يرجى المحاولة مرة أخرى."
},
"mailCategories": {
"primary": "الرئيسية",
@@ -269,7 +272,9 @@
"notFound": "لم يتم العثور على الإعدادات",
"saved": "تم حفظ الإعدادات",
"failedToSave": "فشل في حفظ الإعدادات",
- "languageChanged": "تم تغيير اللغة إلى {locale}"
+ "languageChanged": "تم تغيير اللغة إلى {locale}",
+ "defaultEmailUpdated": "تم تحديث البريد الإلكتروني الافتراضي في جيميل",
+ "failedToUpdateGmailAlias": "فشل تحديث الاسم المستعار الافتراضي في إعدادات جيميل"
},
"mail": {
"replies": "{count, plural, =0 {ردود} one {رد واحد} two {ردان} few {# ردود} many {# رداً} other {# رد}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "خصص كيفية كتابة الذكاء الاصطناعي لردود بريدك الإلكتروني. سيتم إضافة هذا إلى التوجيه الأساسي.",
"noResultsFound": "لم يتم العثور على نتائج",
"zeroSignature": "توقيع Zero",
- "zeroSignatureDescription": "إضافة توقيع Zero إلى رسائل البريد الإلكتروني الخاصة بك."
+ "zeroSignatureDescription": "إضافة توقيع Zero إلى رسائل البريد الإلكتروني الخاصة بك.",
+ "defaultEmailAlias": "الاسم المستعار الافتراضي للبريد الإلكتروني",
+ "selectDefaultEmail": "اختر البريد الإلكتروني الافتراضي",
+ "defaultEmailDescription": "سيتم استخدام هذا البريد الإلكتروني كعنوان \"من\" الافتراضي عند إنشاء رسائل إلكترونية جديدة"
},
"connections": {
"title": "اتصالات البريد الإلكتروني",
diff --git a/apps/mail/locales/ca.json b/apps/mail/locales/ca.json
index 5297bacd90..fe578cacaf 100644
--- a/apps/mail/locales/ca.json
+++ b/apps/mail/locales/ca.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Comunitat",
"documentation": "Documentació",
- "appTheme": "Tema de l'aplicació",
+ "appTheme": "Canvia el tema",
"accounts": "Comptes",
"signIn": "Iniciar la sessió",
- "otherAccounts": "Altres comptes"
+ "otherAccounts": "Altres comptes",
+ "switchingAccounts": "Canviant de comptes...",
+ "accountSwitched": "S'ha canviat de compte correctament",
+ "failedToSwitchAccount": "No s'ha pogut canviar de compte. Torneu-ho a provar."
},
"mailCategories": {
"primary": "Primari",
@@ -269,7 +272,9 @@
"notFound": "Configuració no trobada",
"saved": "Configuració desada",
"failedToSave": "No s'ha pogut desar la configuració",
- "languageChanged": "Idioma canviat a {locale}"
+ "languageChanged": "Idioma canviat a {locale}",
+ "defaultEmailUpdated": "L'àlies de correu predeterminat s'ha actualitzat a Gmail",
+ "failedToUpdateGmailAlias": "No s'ha pogut actualitzar l'àlies predeterminat a la configuració de Gmail"
},
"mail": {
"replies": "{count, plural, =0 {respostes} one {# resposta} other {# respostes}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Personalitza com la IA escriu les teves respostes de correu. Això s'afegirà a la instrucció base.",
"noResultsFound": "No s'han trobat resultats",
"zeroSignature": "Signatura Zero",
- "zeroSignatureDescription": "Afegeix una signatura Zero als teus correus."
+ "zeroSignatureDescription": "Afegeix una signatura Zero als teus correus.",
+ "defaultEmailAlias": "Àlies de correu predeterminat",
+ "selectDefaultEmail": "Selecciona el correu predeterminat",
+ "defaultEmailDescription": "Aquest correu s'utilitzarà com a adreça predeterminada 'De' quan es redactin nous correus"
},
"connections": {
"title": "Connexions Correu",
diff --git a/apps/mail/locales/cs.json b/apps/mail/locales/cs.json
index 4855d30c14..b19256e324 100644
--- a/apps/mail/locales/cs.json
+++ b/apps/mail/locales/cs.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Komunita",
"documentation": "Dokumentace",
- "appTheme": "Vzhled aplikace",
+ "appTheme": "Přepnout motiv",
"accounts": "Účty",
"signIn": "Přihlásit se",
- "otherAccounts": "Další účty"
+ "otherAccounts": "Další účty",
+ "switchingAccounts": "Přepínání účtů...",
+ "accountSwitched": "Účet byl úspěšně přepnut",
+ "failedToSwitchAccount": "Nepodařilo se přepnout účty. Zkuste to prosím znovu."
},
"mailCategories": {
"primary": "Hlavní",
@@ -269,7 +272,9 @@
"notFound": "Nastavení nebylo nalezeno",
"saved": "Nastavení bylo uloženo",
"failedToSave": "Nepodařilo se uložit nastavení",
- "languageChanged": "Jazyk změněn na {locale}"
+ "languageChanged": "Jazyk změněn na {locale}",
+ "defaultEmailUpdated": "Výchozí e-mailový alias byl aktualizován v Gmailu",
+ "failedToUpdateGmailAlias": "Nepodařilo se aktualizovat výchozí alias v nastavení Gmailu"
},
"mail": {
"replies": "{count, plural, =0 {odpovědí} one {#odpověď} few {#odpovědi} many {#odpovědí} other {#odpovědí}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Přizpůsobte, jak AI píše vaše e-mailové odpovědi. Toto bude přidáno k základnímu pokynu.",
"noResultsFound": "Nebyly nalezeny žádné výsledky",
"zeroSignature": "Zero podpis",
- "zeroSignatureDescription": "Přidat Zero podpis k vašim emailům."
+ "zeroSignatureDescription": "Přidat Zero podpis k vašim emailům.",
+ "defaultEmailAlias": "Výchozí e-mailový alias",
+ "selectDefaultEmail": "Vybrat výchozí e-mail",
+ "defaultEmailDescription": "Tento e-mail bude použit jako výchozí adresa 'Od' při psaní nových e-mailů"
},
"connections": {
"title": "Propojení emailu",
diff --git a/apps/mail/locales/de.json b/apps/mail/locales/de.json
index 69cf6e187e..11bd1f409c 100644
--- a/apps/mail/locales/de.json
+++ b/apps/mail/locales/de.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Community",
"documentation": "Dokumentation",
- "appTheme": "App Theme",
+ "appTheme": "Theme wechseln",
"accounts": "Benutzerkonten",
"signIn": "Anmelden",
- "otherAccounts": "Andere Konten"
+ "otherAccounts": "Andere Konten",
+ "switchingAccounts": "Konten werden gewechselt...",
+ "accountSwitched": "Konto erfolgreich gewechselt",
+ "failedToSwitchAccount": "Kontowechsel fehlgeschlagen. Bitte versuche es erneut."
},
"mailCategories": {
"primary": "Primär",
@@ -269,7 +272,9 @@
"notFound": "Einstellungen nicht gefunden",
"saved": "Einstellungen gespeichert",
"failedToSave": "Einstellungen konnten nicht gespeichert werden",
- "languageChanged": "Sprache zu {locale} geändert"
+ "languageChanged": "Sprache zu {locale} geändert",
+ "defaultEmailUpdated": "Standard-E-Mail-Alias in Gmail aktualisiert",
+ "failedToUpdateGmailAlias": "Aktualisierung des Standard-Alias in den Gmail-Einstellungen fehlgeschlagen"
},
"mail": {
"replies": "{count, plural, =0 {Antworten} one {# Antwort} other {# Antworten}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Passen Sie an, wie die KI Ihre E-Mail beantwortet. Dies wird zur Base-Prompt hinzugefügt.",
"noResultsFound": "Keine Ergebnisse gefunden",
"zeroSignature": "Zero-Signatur",
- "zeroSignatureDescription": "Füge deinen E-Mails eine Zero-Signatur hinzu."
+ "zeroSignatureDescription": "Füge deinen E-Mails eine Zero-Signatur hinzu.",
+ "defaultEmailAlias": "Standard-E-Mail-Alias",
+ "selectDefaultEmail": "Standard-E-Mail auswählen",
+ "defaultEmailDescription": "Diese E-Mail wird als Standard-Absenderadresse beim Verfassen neuer E-Mails verwendet"
},
"connections": {
"title": "E-Mail Verbindungen",
diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json
index a5bc5762f8..53c263bed3 100644
--- a/apps/mail/locales/en.json
+++ b/apps/mail/locales/en.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Community",
"documentation": "Documentation",
- "appTheme": "App Theme",
+ "appTheme": "Toggle Theme",
"accounts": "Accounts",
"signIn": "Sign in",
- "otherAccounts": "Other Accounts"
+ "otherAccounts": "Other Accounts",
+ "switchingAccounts": "Switching accounts...",
+ "accountSwitched": "Account switched successfully",
+ "failedToSwitchAccount": "Failed to switch accounts. Please try again."
},
"mailCategories": {
"primary": "Primary",
@@ -269,7 +272,9 @@
"notFound": "Settings not found",
"saved": "Settings saved",
"failedToSave": "Failed to save settings",
- "languageChanged": "Language changed to {locale}"
+ "languageChanged": "Language changed to {locale}",
+ "defaultEmailUpdated": "Default email alias updated in Gmail",
+ "failedToUpdateGmailAlias": "Failed to update default alias in Gmail settings"
},
"mail": {
"replies": "{count, plural, =0 {replies} one {# reply} other {# replies}}",
@@ -328,21 +333,21 @@
"mb": "{amount} MB"
},
"labels": {
- "color":"Color",
- "labelName":"Label Name",
- "createLabel":"Create Label",
+ "color": "Color",
+ "labelName": "Label Name",
+ "createLabel": "Create Label",
"deleteLabel": "Delete Label",
"deleteLabelConfirm": "Are you sure you want to delete this label?",
"deleteLabelConfirmDescription": "This action cannot be undone.",
"deleteLabelConfirmCancel": "Cancel",
"deleteLabelConfirmDelete": "Delete",
"deleteLabelSuccess": "Label deleted successfully",
- "failedToDeleteLabel":"Failed to delete label",
- "deletingLabel":"Deleting label...",
- "editLabel":"Edit Label",
- "savingLabel":"Saving label...",
- "failedToSavingLabel":"Failed to save label",
- "saveLabelSuccess":"Label saved successfully"
+ "failedToDeleteLabel": "Failed to delete label",
+ "deletingLabel": "Deleting label...",
+ "editLabel": "Edit Label",
+ "savingLabel": "Saving label...",
+ "failedToSavingLabel": "Failed to save label",
+ "saveLabelSuccess": "Label saved successfully"
}
},
"navigation": {
@@ -396,7 +401,12 @@
"customPromptDescription": "Customize how the AI writes your email replies. This will be added to the base prompt.",
"noResultsFound": "No results found",
"zeroSignature": "Zero Signature",
- "zeroSignatureDescription": "Add a Zero signature to your emails."
+ "zeroSignatureDescription": "Add a Zero signature to your emails.",
+ "defaultEmailAlias": "Default Email Alias",
+ "selectDefaultEmail": "Select default email",
+ "defaultEmailDescription": "This email will be used as the default 'From' address when composing new emails",
+ "autoRead": "Auto Read",
+ "autoReadDescription": "Automatically mark emails as read when you click on them."
},
"connections": {
"title": "Email Connections",
diff --git a/apps/mail/locales/es.json b/apps/mail/locales/es.json
index f025def2cf..f44fceeaeb 100644
--- a/apps/mail/locales/es.json
+++ b/apps/mail/locales/es.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Comunidad",
"documentation": "Documentación",
- "appTheme": "Tema de aplicación",
+ "appTheme": "Cambiar tema",
"accounts": "Cuentas",
"signIn": "Registrarse",
- "otherAccounts": "Otras cuentas"
+ "otherAccounts": "Otras cuentas",
+ "switchingAccounts": "Cambiando cuentas...",
+ "accountSwitched": "Cuenta cambiada correctamente",
+ "failedToSwitchAccount": "No se pudo cambiar de cuenta. Por favor, inténtalo de nuevo."
},
"mailCategories": {
"primary": "Principal",
@@ -269,7 +272,9 @@
"notFound": "Configuración no encontrada",
"saved": "Configuración guardada",
"failedToSave": "Error al guardar la configuración",
- "languageChanged": "Idioma cambiado a {locale}"
+ "languageChanged": "Idioma cambiado a {locale}",
+ "defaultEmailUpdated": "Alias de correo predeterminado actualizado en Gmail",
+ "failedToUpdateGmailAlias": "No se pudo actualizar el alias predeterminado en la configuración de Gmail"
},
"mail": {
"replies": "{count, plural, =0 {respuestas} one {# respuesta} other {# respuestas}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Personaliza cómo la IA escribe tus respuestas de correo electrónico. Esto se añadirá al prompt base.",
"noResultsFound": "No se han encontrado resultados",
"zeroSignature": "Firma Zero",
- "zeroSignatureDescription": "Añade una firma Zero a tus correos."
+ "zeroSignatureDescription": "Añade una firma Zero a tus correos.",
+ "defaultEmailAlias": "Alias de correo electrónico predeterminado",
+ "selectDefaultEmail": "Seleccionar correo predeterminado",
+ "defaultEmailDescription": "Este correo electrónico se utilizará como dirección predeterminada 'De' al redactar nuevos correos"
},
"connections": {
"title": "Conexiones de correo",
diff --git a/apps/mail/locales/fa.json b/apps/mail/locales/fa.json
index 811e110d83..558d937ee1 100644
--- a/apps/mail/locales/fa.json
+++ b/apps/mail/locales/fa.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "انجمن",
"documentation": "مستندات",
- "appTheme": "تم برنامه",
+ "appTheme": "تغییر پوسته",
"accounts": "حسابها",
"signIn": "ورود",
- "otherAccounts": "حسابهای دیگر"
+ "otherAccounts": "حسابهای دیگر",
+ "switchingAccounts": "در حال تغییر حسابها...",
+ "accountSwitched": "حساب با موفقیت تغییر کرد",
+ "failedToSwitchAccount": "تغییر حساب ناموفق بود. لطفاً دوباره تلاش کنید."
},
"mailCategories": {
"primary": "اصلی",
@@ -269,7 +272,9 @@
"notFound": "تنظیمات یافت نشد",
"saved": "تنظیمات ذخیره شد",
"failedToSave": "ذخیره تنظیمات ناموفق بود",
- "languageChanged": "زبان به {locale} تغییر کرد"
+ "languageChanged": "زبان به {locale} تغییر کرد",
+ "defaultEmailUpdated": "نام مستعار ایمیل پیشفرض در جیمیل بهروزرسانی شد",
+ "failedToUpdateGmailAlias": "بهروزرسانی نام مستعار پیشفرض در تنظیمات جیمیل ناموفق بود"
},
"mail": {
"replies": "{count, plural, =0 {پاسخها} one {# پاسخ} other {# پاسخ}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "نحوه نگارش پاسخهای ایمیل توسط هوش مصنوعی را سفارشی کنید. این به پرامپت پایه اضافه خواهد شد.",
"noResultsFound": "نتیجهای یافت نشد",
"zeroSignature": "امضای Zero",
- "zeroSignatureDescription": "افزودن امضای Zero به ایمیلهای شما."
+ "zeroSignatureDescription": "افزودن امضای Zero به ایمیلهای شما.",
+ "defaultEmailAlias": "نام مستعار ایمیل پیشفرض",
+ "selectDefaultEmail": "انتخاب ایمیل پیشفرض",
+ "defaultEmailDescription": "این ایمیل به عنوان آدرس پیشفرض 'از طرف' هنگام نوشتن ایمیلهای جدید استفاده خواهد شد"
},
"connections": {
"title": "اتصالات ایمیل",
diff --git a/apps/mail/locales/fr.json b/apps/mail/locales/fr.json
index 9a6421a9b2..328f3c5d04 100644
--- a/apps/mail/locales/fr.json
+++ b/apps/mail/locales/fr.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Communauté",
"documentation": "Documentation",
- "appTheme": "Thème de l'application",
+ "appTheme": "Changer de thème",
"accounts": "Comptes",
"signIn": "Se connecter",
- "otherAccounts": "Autres comptes"
+ "otherAccounts": "Autres comptes",
+ "switchingAccounts": "Changement de compte...",
+ "accountSwitched": "Compte changé avec succès",
+ "failedToSwitchAccount": "Échec du changement de compte. Veuillez réessayer."
},
"mailCategories": {
"primary": "Principale",
@@ -269,7 +272,9 @@
"notFound": "Paramètres introuvables",
"saved": "Paramètres enregistrés",
"failedToSave": "Échec de la sauvegarde des paramètres",
- "languageChanged": "Langue changée en {locale}"
+ "languageChanged": "Langue changée en {locale}",
+ "defaultEmailUpdated": "Adresse e-mail par défaut mise à jour dans Gmail",
+ "failedToUpdateGmailAlias": "Échec de la mise à jour de l'alias par défaut dans les paramètres Gmail"
},
"mail": {
"replies": "{count, plural, =0 {réponses} one {# réponse} other {# réponses}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Personnalisez la façon dont l'IA écrit vos réponses. Cela sera ajouté à la requête par défaut.",
"noResultsFound": "Aucun résultat trouvé",
"zeroSignature": "Signature Zero",
- "zeroSignatureDescription": "Ajouter une signature Zero à vos courriels."
+ "zeroSignatureDescription": "Ajouter une signature Zero à vos courriels.",
+ "defaultEmailAlias": "Alias e-mail par défaut",
+ "selectDefaultEmail": "Sélectionner l'e-mail par défaut",
+ "defaultEmailDescription": "Cette adresse e-mail sera utilisée comme adresse d'expédition par défaut lors de la rédaction de nouveaux e-mails"
},
"connections": {
"title": "Connexions de courriels",
diff --git a/apps/mail/locales/hi.json b/apps/mail/locales/hi.json
index b8bf3d1524..f7da8fb286 100644
--- a/apps/mail/locales/hi.json
+++ b/apps/mail/locales/hi.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "समुदाय",
"documentation": "डॉक्यूमेंटेशन",
- "appTheme": "एप थीम",
+ "appTheme": "थीम टॉगल करें",
"accounts": "खाता",
"signIn": "साइन इन करें",
- "otherAccounts": "अन्य खाते"
+ "otherAccounts": "अन्य खाते",
+ "switchingAccounts": "खाते स्विच कर रहे हैं...",
+ "accountSwitched": "खाता सफलतापूर्वक स्विच किया गया",
+ "failedToSwitchAccount": "खाते स्विच करने में विफल। कृपया पुनः प्रयास करें।"
},
"mailCategories": {
"primary": "प्राइमरी",
@@ -269,7 +272,9 @@
"notFound": "सेटिंग्स नहीं मिली",
"saved": "सेटिंग्स सहेजी गई",
"failedToSave": "सेटिंग्स सहेजने में विफल",
- "languageChanged": "भाषा बदलकर {locale} कर दी गई"
+ "languageChanged": "भाषा बदलकर {locale} कर दी गई",
+ "defaultEmailUpdated": "Gmail में डिफ़ॉल्ट ईमेल उपनाम अपडेट किया गया",
+ "failedToUpdateGmailAlias": "Gmail सेटिंग्स में डिफ़ॉल्ट उपनाम अपडेट करने में विफल"
},
"mail": {
"replies": "{count, plural, =0 {जवाब} one {# जवाब} other {# जवाब}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "AI आपके ईमेल जवाब कैसे लिखता है, इसे अनुकूलित करें। यह आधार प्रॉम्प्ट में जोड़ा जाएगा।",
"noResultsFound": "कोई परिणाम नहीं मिला",
"zeroSignature": "ज़ीरो सिग्नेचर",
- "zeroSignatureDescription": "अपने ईमेल में ज़ीरो सिग्नेचर जोड़ें।"
+ "zeroSignatureDescription": "अपने ईमेल में ज़ीरो सिग्नेचर जोड़ें।",
+ "defaultEmailAlias": "डिफ़ॉल्ट ईमेल उपनाम",
+ "selectDefaultEmail": "डिफ़ॉल्ट ईमेल चुनें",
+ "defaultEmailDescription": "नए ईमेल लिखते समय यह ईमेल डिफ़ॉल्ट 'प्रेषक' पते के रूप में उपयोग किया जाएगा"
},
"connections": {
"title": "ईमेल कनेक्शन्स",
diff --git a/apps/mail/locales/hu.json b/apps/mail/locales/hu.json
index bab3f263d9..9e1e8ff76f 100644
--- a/apps/mail/locales/hu.json
+++ b/apps/mail/locales/hu.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Közösség",
"documentation": "Dokumentáció",
- "appTheme": "Alkalmazás téma",
+ "appTheme": "Téma váltása",
"accounts": "Fiókok",
"signIn": "Bejelentkezés",
- "otherAccounts": "Egyéb fiókok"
+ "otherAccounts": "Egyéb fiókok",
+ "switchingAccounts": "Fiókok közötti váltás...",
+ "accountSwitched": "Fiókváltás sikeres",
+ "failedToSwitchAccount": "Nem sikerült fiókot váltani. Kérjük, próbálja újra."
},
"mailCategories": {
"primary": "Elsődleges",
@@ -269,7 +272,9 @@
"notFound": "Beállítások nem találhatók",
"saved": "Beállítások mentve",
"failedToSave": "Nem sikerült menteni a beállításokat",
- "languageChanged": "Nyelv megváltoztatva erre: {locale}"
+ "languageChanged": "Nyelv megváltoztatva erre: {locale}",
+ "defaultEmailUpdated": "Az alapértelmezett e-mail álnév frissítve a Gmailben",
+ "failedToUpdateGmailAlias": "Nem sikerült frissíteni az alapértelmezett álnevet a Gmail beállításaiban"
},
"mail": {
"replies": "{count, plural, =0 {válasz} one {# válasz} other {# válasz}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Szabd testre, hogyan írja az AI az e-mail válaszaidat. Ez hozzáadódik az alap utasításhoz.",
"noResultsFound": "Nincs találat",
"zeroSignature": "Zero aláírás",
- "zeroSignatureDescription": "Zero aláírás hozzáadása az e-mailjeihez."
+ "zeroSignatureDescription": "Zero aláírás hozzáadása az e-mailjeihez.",
+ "defaultEmailAlias": "Alapértelmezett e-mail álnév",
+ "selectDefaultEmail": "Alapértelmezett e-mail kiválasztása",
+ "defaultEmailDescription": "Ez az e-mail lesz az alapértelmezett 'Feladó' cím új e-mailek írásakor"
},
"connections": {
"title": "E-mail kapcsolatok",
diff --git a/apps/mail/locales/ja.json b/apps/mail/locales/ja.json
index 043cc00bc4..eac26f5463 100644
--- a/apps/mail/locales/ja.json
+++ b/apps/mail/locales/ja.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "コミュニティ",
"documentation": "ドキュメント",
- "appTheme": "テーマ",
+ "appTheme": "テーマを切り替え",
"accounts": "アカウント",
"signIn": "サインイン",
- "otherAccounts": "その他のアカウント"
+ "otherAccounts": "その他のアカウント",
+ "switchingAccounts": "アカウント切り替え中...",
+ "accountSwitched": "アカウントの切り替えに成功しました",
+ "failedToSwitchAccount": "アカウントの切り替えに失敗しました。もう一度お試しください。"
},
"mailCategories": {
"primary": "プライマリ",
@@ -269,7 +272,9 @@
"notFound": "設定が見つかりません",
"saved": "設定が保存されました",
"failedToSave": "設定の保存に失敗しました",
- "languageChanged": "言語を {locale}に変更しました"
+ "languageChanged": "言語を {locale}に変更しました",
+ "defaultEmailUpdated": "Gmailでデフォルトのメールエイリアスが更新されました",
+ "failedToUpdateGmailAlias": "Gmail設定でデフォルトのエイリアスの更新に失敗しました"
},
"mail": {
"replies": "{count, plural, =0 {replies} other {# replies}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "AIがメールの返信を書く方法をカスタマイズします。これはベースプロンプトに追加されます。",
"noResultsFound": "結果が見つかりませんでした",
"zeroSignature": "Zero署名",
- "zeroSignatureDescription": "メールにZero署名を追加します。"
+ "zeroSignatureDescription": "メールにZero署名を追加します。",
+ "defaultEmailAlias": "デフォルトのメールエイリアス",
+ "selectDefaultEmail": "デフォルトのメールを選択",
+ "defaultEmailDescription": "このメールは新しいメールを作成する際のデフォルトの「差出人」アドレスとして使用されます"
},
"connections": {
"title": "メールの接続",
diff --git a/apps/mail/locales/ko.json b/apps/mail/locales/ko.json
index 465b5d2ecf..c43053c097 100644
--- a/apps/mail/locales/ko.json
+++ b/apps/mail/locales/ko.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "커뮤니티",
"documentation": "문서",
- "appTheme": "앱 테마",
+ "appTheme": "테마 전환",
"accounts": "계정",
"signIn": "로그인",
- "otherAccounts": "다른 계정"
+ "otherAccounts": "다른 계정",
+ "switchingAccounts": "계정 전환 중...",
+ "accountSwitched": "계정이 성공적으로 전환되었습니다",
+ "failedToSwitchAccount": "계정 전환에 실패했습니다. 다시 시도해 주세요."
},
"mailCategories": {
"primary": "기본",
@@ -269,7 +272,9 @@
"notFound": "설정을 찾을 수 없습니다",
"saved": "설정 저장됨",
"failedToSave": "설정 저장 실패",
- "languageChanged": "언어가 {locale}(으)로 변경되었습니다"
+ "languageChanged": "언어가 {locale}(으)로 변경되었습니다",
+ "defaultEmailUpdated": "Gmail에서 기본 이메일 별칭이 업데이트되었습니다",
+ "failedToUpdateGmailAlias": "Gmail 설정에서 기본 별칭 업데이트에 실패했습니다"
},
"mail": {
"replies": "{count, plural, =0 {답장} one {답장 #개} other {답장 #개}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "AI가 이메일 답장을 작성하는 방식을 맞춤설정하세요. 이것은 기본 프롬프트에 추가됩니다.",
"noResultsFound": "검색 결과 없음",
"zeroSignature": "Zero 서명",
- "zeroSignatureDescription": "이메일에 Zero 서명을 추가합니다."
+ "zeroSignatureDescription": "이메일에 Zero 서명을 추가합니다.",
+ "defaultEmailAlias": "기본 이메일 별칭",
+ "selectDefaultEmail": "기본 이메일 선택",
+ "defaultEmailDescription": "이 이메일은 새 이메일 작성 시 기본 '보낸 사람' 주소로 사용됩니다"
},
"connections": {
"title": "이메일 연결",
diff --git a/apps/mail/locales/lv.json b/apps/mail/locales/lv.json
index 01c43b97a9..d8a4a21abe 100644
--- a/apps/mail/locales/lv.json
+++ b/apps/mail/locales/lv.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Kopiena",
"documentation": "Dokumentācija",
- "appTheme": "Aplikācijas motīvs",
+ "appTheme": "Pārslēgt krāsu tēmu",
"accounts": "Konti",
"signIn": "Pierakstīties",
- "otherAccounts": "Citi konti"
+ "otherAccounts": "Citi konti",
+ "switchingAccounts": "Pārslēdz kontus...",
+ "accountSwitched": "Konts veiksmīgi pārslēgts",
+ "failedToSwitchAccount": "Neizdevās pārslēgt kontus. Lūdzu, mēģiniet vēlreiz."
},
"mailCategories": {
"primary": "Galvenā",
@@ -269,7 +272,9 @@
"notFound": "Iestatījumu lapa nav atrasta",
"saved": "Iestatījumi saglabāti",
"failedToSave": "Neizdevās saglabāt iestatījumus",
- "languageChanged": "Valoda nomainīta uz {locale}"
+ "languageChanged": "Valoda nomainīta uz {locale}",
+ "defaultEmailUpdated": "Noklusējuma e-pasta adreses aizstājvārds atjaunināts Gmail",
+ "failedToUpdateGmailAlias": "Neizdevās atjaunināt noklusējuma aizstājvārdu Gmail iestatījumos"
},
"mail": {
"replies": "{count, plural, zero {}=0 {atbildes} one {# atbilde} other {# atbildes}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Pielāgot, kā AI raksta jūsu e-pasta atbildes. Jūsu norādījumi tiks pievienoti AI noklusējuma instrukcijām.",
"noResultsFound": "Netika atrasts neviens rezultāts",
"zeroSignature": "Zero paraksts",
- "zeroSignatureDescription": "Pievienot Zero parakstu saviem e-pastiem."
+ "zeroSignatureDescription": "Pievienot Zero parakstu saviem e-pastiem.",
+ "defaultEmailAlias": "Noklusējuma e-pasta aizstājvārds",
+ "selectDefaultEmail": "Izvēlieties noklusējuma e-pastu",
+ "defaultEmailDescription": "Šis e-pasts tiks izmantots kā noklusējuma 'No' adrese, rakstot jaunus e-pastus"
},
"connections": {
"title": "E-pasta savienojumi",
diff --git a/apps/mail/locales/nl.json b/apps/mail/locales/nl.json
index 956f4f5145..fffa7b746f 100644
--- a/apps/mail/locales/nl.json
+++ b/apps/mail/locales/nl.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Community",
"documentation": "Documentatie",
- "appTheme": "App-thema",
+ "appTheme": "Thema wisselen",
"accounts": "Accounts",
"signIn": "Inloggen",
- "otherAccounts": "Andere accounts"
+ "otherAccounts": "Andere accounts",
+ "switchingAccounts": "Accounts wisselen...",
+ "accountSwitched": "Account succesvol gewisseld",
+ "failedToSwitchAccount": "Kon niet van account wisselen. Probeer het opnieuw."
},
"mailCategories": {
"primary": "Primair",
@@ -269,7 +272,9 @@
"notFound": "Instellingen niet gevonden",
"saved": "Instellingen opgeslagen",
"failedToSave": "Instellingen opslaan mislukt",
- "languageChanged": "Taal gewijzigd naar {locale}"
+ "languageChanged": "Taal gewijzigd naar {locale}",
+ "defaultEmailUpdated": "Standaard e-mailalias bijgewerkt in Gmail",
+ "failedToUpdateGmailAlias": "Kon standaardalias niet bijwerken in Gmail-instellingen"
},
"mail": {
"replies": "{count, plural, =0 {reacties} one {# reactie} other {# reacties}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Pas aan hoe de AI je e-mailantwoorden schrijft. Dit wordt toegevoegd aan de basisprompt.",
"noResultsFound": "Geen resultaten gevonden",
"zeroSignature": "Zero-handtekening",
- "zeroSignatureDescription": "Voeg een Zero-handtekening toe aan je e-mails."
+ "zeroSignatureDescription": "Voeg een Zero-handtekening toe aan je e-mails.",
+ "defaultEmailAlias": "Standaard e-mailalias",
+ "selectDefaultEmail": "Selecteer standaard e-mail",
+ "defaultEmailDescription": "Dit e-mailadres wordt gebruikt als standaard 'Van'-adres bij het opstellen van nieuwe e-mails"
},
"connections": {
"title": "E-mailverbindingen",
diff --git a/apps/mail/locales/pl.json b/apps/mail/locales/pl.json
index 6f71485698..2baa77eb13 100644
--- a/apps/mail/locales/pl.json
+++ b/apps/mail/locales/pl.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Społeczność",
"documentation": "Dokumentacja",
- "appTheme": "Motyw aplikacji",
+ "appTheme": "Przełącz motyw",
"accounts": "Konta",
"signIn": "Zaloguj się",
- "otherAccounts": "Inne konta"
+ "otherAccounts": "Inne konta",
+ "switchingAccounts": "Przełączanie kont...",
+ "accountSwitched": "Konto zostało pomyślnie przełączone",
+ "failedToSwitchAccount": "Nie udało się przełączyć kont. Spróbuj ponownie."
},
"mailCategories": {
"primary": "Główne",
@@ -269,7 +272,9 @@
"notFound": "Nie znaleziono ustawień",
"saved": "Ustawienia zapisane",
"failedToSave": "Nie udało się zapisać ustawień",
- "languageChanged": "Język zmieniony na {locale}"
+ "languageChanged": "Język zmieniony na {locale}",
+ "defaultEmailUpdated": "Domyślny alias e-mail został zaktualizowany w Gmailu",
+ "failedToUpdateGmailAlias": "Nie udało się zaktualizować domyślnego aliasu w ustawieniach Gmaila"
},
"mail": {
"replies": "{count, plural, =0 {odpowiedzi} one {# odpowiedź} few {# odpowiedzi} many {# odpowiedzi} other {# odpowiedzi}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Dostosuj sposób, w jaki AI pisze odpowiedzi na Twoje e-maile. Zostanie to dodane do podstawowego polecenia.",
"noResultsFound": "Nie znaleziono wyników",
"zeroSignature": "Podpis Zero",
- "zeroSignatureDescription": "Dodaj podpis Zero do swoich e-maili."
+ "zeroSignatureDescription": "Dodaj podpis Zero do swoich e-maili.",
+ "defaultEmailAlias": "Domyślny alias e-mail",
+ "selectDefaultEmail": "Wybierz domyślny e-mail",
+ "defaultEmailDescription": "Ten e-mail będzie używany jako domyślny adres 'Od' podczas tworzenia nowych wiadomości e-mail"
},
"connections": {
"title": "Połączenia e-mail",
diff --git a/apps/mail/locales/pt.json b/apps/mail/locales/pt.json
index edbdf9f376..16c4e31569 100644
--- a/apps/mail/locales/pt.json
+++ b/apps/mail/locales/pt.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Comunidade",
"documentation": "Documentação",
- "appTheme": "Tema da aplicação",
+ "appTheme": "Alternar o Tema",
"accounts": "Contas",
"signIn": "Iniciar sessão",
- "otherAccounts": "Outras Contas"
+ "otherAccounts": "Outras Contas",
+ "switchingAccounts": "Alternando contas...",
+ "accountSwitched": "Conta alternada com sucesso",
+ "failedToSwitchAccount": "Falha ao alternar contas. Por favor, tente novamente."
},
"mailCategories": {
"primary": "Principal",
@@ -269,7 +272,9 @@
"notFound": "Configurações não encontradas",
"saved": "Configurações salvas",
"failedToSave": "Falha ao salvar configurações",
- "languageChanged": "Idioma alterado para {locale}"
+ "languageChanged": "Idioma alterado para {locale}",
+ "defaultEmailUpdated": "Alias de email padrão atualizado no Gmail",
+ "failedToUpdateGmailAlias": "Falha ao atualizar o alias padrão nas configurações do Gmail"
},
"mail": {
"replies": "{count, plural, =0 {arquivos} one {arquivo} other {arquivos}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Personalize como a IA escreve suas respostas de email. Isto será adicionado ao prompt base.",
"noResultsFound": "Nenhum resultado encontrado",
"zeroSignature": "Assinatura Zero",
- "zeroSignatureDescription": "Adicionar uma assinatura Zero aos seus e-mails."
+ "zeroSignatureDescription": "Adicionar uma assinatura Zero aos seus e-mails.",
+ "defaultEmailAlias": "Alias de Email Padrão",
+ "selectDefaultEmail": "Selecionar email padrão",
+ "defaultEmailDescription": "Este email será usado como endereço 'De' padrão ao compor novos emails"
},
"connections": {
"title": "Conexões de E-mail",
diff --git a/apps/mail/locales/ru.json b/apps/mail/locales/ru.json
index a8def5a616..498f879145 100644
--- a/apps/mail/locales/ru.json
+++ b/apps/mail/locales/ru.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Сообщество",
"documentation": "Документация",
- "appTheme": "Тема",
+ "appTheme": "Переключить тему",
"accounts": "Аккаунты",
"signIn": "Войти",
- "otherAccounts": "Другие аккаунты"
+ "otherAccounts": "Другие аккаунты",
+ "switchingAccounts": "Переключение между аккаунтами...",
+ "accountSwitched": "Аккаунт успешно переключен",
+ "failedToSwitchAccount": "Не удалось переключить аккаунты. Пожалуйста, попробуйте снова."
},
"mailCategories": {
"primary": "Основные",
@@ -269,7 +272,9 @@
"notFound": "Настройки не найдены",
"saved": "Настройки сохранены",
"failedToSave": "Не удалось сохранить настройки",
- "languageChanged": "Язык изменен на {locale}"
+ "languageChanged": "Язык изменен на {locale}",
+ "defaultEmailUpdated": "Адрес электронной почты по умолчанию обновлен в Gmail",
+ "failedToUpdateGmailAlias": "Не удалось обновить адрес по умолчанию в настройках Gmail"
},
"mail": {
"replies": "{count, plural, =0 {ответы} one {# ответ} few {# ответа} many {# ответов} other {# ответы}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Настройте, как ИИ будет писать ваши ответы на письма. Этот текст будет добавлен к основному запросу.",
"noResultsFound": "Результаты не найдены",
"zeroSignature": "Подпись Zero",
- "zeroSignatureDescription": "Добавьте подпись Zero к вашим письмам."
+ "zeroSignatureDescription": "Добавьте подпись Zero к вашим письмам.",
+ "defaultEmailAlias": "Адрес электронной почты по умолчанию",
+ "selectDefaultEmail": "Выберите адрес электронной почты по умолчанию",
+ "defaultEmailDescription": "Этот адрес будет использоваться в качестве адреса отправителя по умолчанию при создании новых писем"
},
"connections": {
"title": "Подключения",
diff --git a/apps/mail/locales/tr.json b/apps/mail/locales/tr.json
index 687c8e4b14..49248ee73a 100644
--- a/apps/mail/locales/tr.json
+++ b/apps/mail/locales/tr.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Topluluk",
"documentation": "Dokümantasyon",
- "appTheme": "Uygulama Teması",
+ "appTheme": "Temayı Değiştir",
"accounts": "Hesaplar",
"signIn": "Giriş Yap",
- "otherAccounts": "Diğer Hesaplar"
+ "otherAccounts": "Diğer Hesaplar",
+ "switchingAccounts": "Hesaplar değiştiriliyor...",
+ "accountSwitched": "Hesap başarıyla değiştirildi",
+ "failedToSwitchAccount": "Hesap değiştirilemedi. Lütfen tekrar deneyin."
},
"mailCategories": {
"primary": "Birincil",
@@ -269,7 +272,9 @@
"notFound": "Ayarlar bulunamadı",
"saved": "Ayarlar kaydedildi",
"failedToSave": "Ayarlar kaydedilemedi",
- "languageChanged": "Dil {locale} olarak değiştirildi"
+ "languageChanged": "Dil {locale} olarak değiştirildi",
+ "defaultEmailUpdated": "Varsayılan e-posta adresi Gmail'de güncellendi",
+ "failedToUpdateGmailAlias": "Gmail ayarlarında varsayılan adres güncellenemedi"
},
"mail": {
"replies": "{count, plural, =0 {cevaplar} one {# cevap} other {# cevaplar}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Yapay zekanın e-posta yanıtlarınızı nasıl yazacağını özelleştirin. Bu, temel yönergeye eklenecektir.",
"noResultsFound": "Sonuç bulunamadı",
"zeroSignature": "Zero İmzası",
- "zeroSignatureDescription": "E-postalarınıza Zero imzası ekleyin."
+ "zeroSignatureDescription": "E-postalarınıza Zero imzası ekleyin.",
+ "defaultEmailAlias": "Varsayılan E-posta Adresi",
+ "selectDefaultEmail": "Varsayılan e-postayı seç",
+ "defaultEmailDescription": "Bu e-posta, yeni e-postalar oluştururken varsayılan 'Kimden' adresi olarak kullanılacaktır"
},
"connections": {
"title": "E-posta Bağlantıları",
diff --git a/apps/mail/locales/vi.json b/apps/mail/locales/vi.json
index e1175bb4c3..c90b5ddf46 100644
--- a/apps/mail/locales/vi.json
+++ b/apps/mail/locales/vi.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "Cộng đồng",
"documentation": "Tài liệu",
- "appTheme": "Giao diện ứng dụng",
+ "appTheme": "Chuyển đổi giao diện",
"accounts": "Tài khoản",
"signIn": "Đăng nhập",
- "otherAccounts": "Tài khoản khác"
+ "otherAccounts": "Tài khoản khác",
+ "switchingAccounts": "Đang chuyển đổi tài khoản...",
+ "accountSwitched": "Chuyển đổi tài khoản thành công",
+ "failedToSwitchAccount": "Không thể chuyển đổi tài khoản. Vui lòng thử lại."
},
"mailCategories": {
"primary": "Chính",
@@ -269,7 +272,9 @@
"notFound": "Không tìm thấy cài đặt",
"saved": "Đã lưu cài đặt",
"failedToSave": "Không thể lưu cài đặt",
- "languageChanged": "Đã chuyển ngôn ngữ sang {locale}"
+ "languageChanged": "Đã chuyển ngôn ngữ sang {locale}",
+ "defaultEmailUpdated": "Đã cập nhật bí danh email mặc định trong Gmail",
+ "failedToUpdateGmailAlias": "Không thể cập nhật bí danh mặc định trong cài đặt Gmail"
},
"mail": {
"replies": "{count, plural, =0 {phản hồi} one {# phản hồi} other {# phản hồi}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "Tùy chỉnh cách AI viết các trả lời email của bạn. Điều này sẽ được thêm vào lệnh cơ bản.",
"noResultsFound": "Không tìm thấy kết quả nào",
"zeroSignature": "Chữ ký Zero",
- "zeroSignatureDescription": "Thêm chữ ký Zero vào email của bạn."
+ "zeroSignatureDescription": "Thêm chữ ký Zero vào email của bạn.",
+ "defaultEmailAlias": "Bí danh email mặc định",
+ "selectDefaultEmail": "Chọn email mặc định",
+ "defaultEmailDescription": "Email này sẽ được sử dụng làm địa chỉ 'Từ' mặc định khi soạn email mới"
},
"connections": {
"title": "Kết nối email",
diff --git a/apps/mail/locales/zh_CN.json b/apps/mail/locales/zh_CN.json
index 5fcc7194e7..b198cfa712 100644
--- a/apps/mail/locales/zh_CN.json
+++ b/apps/mail/locales/zh_CN.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "社区",
"documentation": "文档",
- "appTheme": "应用主题",
+ "appTheme": "切换主题",
"accounts": "账户",
"signIn": "登录",
- "otherAccounts": "其他账户"
+ "otherAccounts": "其他账户",
+ "switchingAccounts": "正在切换账户...",
+ "accountSwitched": "账户切换成功",
+ "failedToSwitchAccount": "账户切换失败,请重试。"
},
"mailCategories": {
"primary": "主要",
@@ -269,7 +272,9 @@
"notFound": "未找到设置",
"saved": "设置已保存",
"failedToSave": "保存设置失败",
- "languageChanged": "语言已更改为{locale}"
+ "languageChanged": "语言已更改为{locale}",
+ "defaultEmailUpdated": "默认邮箱别名已在 Gmail 中更新",
+ "failedToUpdateGmailAlias": "无法更新 Gmail 设置中的默认别名"
},
"mail": {
"replies": "{count, plural, =0 {回复} one {# 条回复} other {# 条回复}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "自定义AI如何撰写您的邮件回复。这将添加到基础提示中。",
"noResultsFound": "未找到结果",
"zeroSignature": "Zero 签名",
- "zeroSignatureDescription": "在您的邮件中添加 Zero 签名。"
+ "zeroSignatureDescription": "在您的邮件中添加 Zero 签名。",
+ "defaultEmailAlias": "默认邮箱别名",
+ "selectDefaultEmail": "选择默认邮箱",
+ "defaultEmailDescription": "此邮箱将作为撰写新邮件时的默认“发件人”地址"
},
"connections": {
"title": "邮箱连接",
diff --git a/apps/mail/locales/zh_TW.json b/apps/mail/locales/zh_TW.json
index fb1b25a8ea..bccd600c97 100644
--- a/apps/mail/locales/zh_TW.json
+++ b/apps/mail/locales/zh_TW.json
@@ -135,10 +135,13 @@
"navUser": {
"customerSupport": "社群",
"documentation": "文件",
- "appTheme": "應用程式主題",
+ "appTheme": "切換主題",
"accounts": "帳戶",
"signIn": "登入",
- "otherAccounts": "其他帳戶"
+ "otherAccounts": "其他帳戶",
+ "switchingAccounts": "正在切換帳戶...",
+ "accountSwitched": "帳戶已成功切換",
+ "failedToSwitchAccount": "切換帳戶失敗。請再試一次。"
},
"mailCategories": {
"primary": "主要",
@@ -269,7 +272,9 @@
"notFound": "找不到設定",
"saved": "設定已儲存",
"failedToSave": "儲存設定失敗",
- "languageChanged": "語言已更改為 {locale}"
+ "languageChanged": "語言已更改為 {locale}",
+ "defaultEmailUpdated": "Gmail 中的預設電子郵件別名已更新",
+ "failedToUpdateGmailAlias": "無法更新 Gmail 設定中的預設別名"
},
"mail": {
"replies": "{count, plural, =0 {回覆} one {# 則回覆} other {# 則回覆}}",
@@ -396,7 +401,10 @@
"customPromptDescription": "自訂 AI 撰寫電子郵件回覆的方式。這將添加到基本提示中。",
"noResultsFound": "找不到結果",
"zeroSignature": "Zero 簽名檔",
- "zeroSignatureDescription": "在您的電子郵件中新增 Zero 簽名檔。"
+ "zeroSignatureDescription": "在您的電子郵件中新增 Zero 簽名檔。",
+ "defaultEmailAlias": "預設電子郵件別名",
+ "selectDefaultEmail": "選擇預設電子郵件",
+ "defaultEmailDescription": "此電子郵件將作為撰寫新郵件時的預設「寄件者」地址"
},
"connections": {
"title": "電子郵件連接",
diff --git a/apps/mail/package.json b/apps/mail/package.json
index 69fef426d8..c734df4f9f 100644
--- a/apps/mail/package.json
+++ b/apps/mail/package.json
@@ -34,6 +34,8 @@
"@tiptap/core": "2.11.5",
"@tiptap/extension-bold": "2.11.5",
"@tiptap/extension-document": "2.11.5",
+ "@tiptap/extension-file-handler": "2.22.3",
+ "@tiptap/extension-image": "2.22.3",
"@tiptap/extension-link": "2.11.5",
"@tiptap/extension-paragraph": "2.11.5",
"@tiptap/extension-placeholder": "2.11.5",
diff --git a/apps/mail/providers/client-providers.tsx b/apps/mail/providers/client-providers.tsx
index 78368fb314..c2692908bf 100644
--- a/apps/mail/providers/client-providers.tsx
+++ b/apps/mail/providers/client-providers.tsx
@@ -1,6 +1,7 @@
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
import { SidebarProvider } from '@/components/ui/sidebar';
import { PostHogProvider } from '@/lib/posthog-provider';
+import { LoadingProvider } from '@/components/context/loading-context';
import { useSettings } from '@/hooks/use-settings';
import { Provider as JotaiProvider } from 'jotai';
import type { PropsWithChildren } from 'react';
@@ -23,8 +24,10 @@ export function ClientProviders({ children }: PropsWithChildren) {
>
- {children}
-
+
+ {children}
+
+
diff --git a/apps/mail/types/index.ts b/apps/mail/types/index.ts
index 9c7f74859a..739c76c493 100644
--- a/apps/mail/types/index.ts
+++ b/apps/mail/types/index.ts
@@ -91,6 +91,8 @@ export interface IConnection {
email: string;
name?: string;
picture?: string;
+ createdAt: Date;
+ providerId: string;
}
export interface Attachment {
diff --git a/apps/server/package.json b/apps/server/package.json
index 47831b3b7c..229b2167e9 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -68,13 +68,16 @@
"superjson": "catalog:",
"twilio": "5.7.0",
"wrangler": "catalog:",
- "zod": "catalog:"
+ "zod": "catalog:",
+ "uuid": "11.1.0",
+ "mime-types": "3.0.1"
},
"devDependencies": {
"@types/he": "1.2.3",
"@types/node": "^22.9.0",
"@types/react": "19.1.6",
"@types/sanitize-html": "2.13.0",
+ "@types/uuid": "10.0.0",
"@zero/eslint-config": "workspace:*",
"@zero/tsconfig": "workspace:*",
"drizzle-kit": "catalog:",
diff --git a/apps/server/src/db/index.ts b/apps/server/src/db/index.ts
index a52b04dc22..558de657c0 100644
--- a/apps/server/src/db/index.ts
+++ b/apps/server/src/db/index.ts
@@ -1,11 +1,13 @@
import { drizzle } from 'drizzle-orm/postgres-js';
+import postgres, { type Sql } from 'postgres';
import * as schema from './schema';
-import postgres from 'postgres';
+
+const createDrizzle = (conn: Sql) => drizzle(conn, { schema });
export const createDb = (url: string) => {
const conn = postgres(url);
- const db = drizzle(conn, { schema });
- return db;
+ const db = createDrizzle(conn);
+ return { db, conn };
};
-export type DB = ReturnType;
+export type DB = ReturnType;
diff --git a/apps/server/src/db/migrations/0029_thin_triathlon.sql b/apps/server/src/db/migrations/0029_thin_triathlon.sql
new file mode 100644
index 0000000000..997f01bdd9
--- /dev/null
+++ b/apps/server/src/db/migrations/0029_thin_triathlon.sql
@@ -0,0 +1 @@
+ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"autoRead":true}'::jsonb;
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/0030_blue_grandmaster.sql b/apps/server/src/db/migrations/0030_blue_grandmaster.sql
new file mode 100644
index 0000000000..efcaa8ea82
--- /dev/null
+++ b/apps/server/src/db/migrations/0030_blue_grandmaster.sql
@@ -0,0 +1 @@
+ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"defaultEmailAlias":"","categories":[{"id":"Important","name":"Important","searchValue":"is:important NOT is:sent NOT is:draft","order":0,"isDefault":false},{"id":"All Mail","name":"All Mail","searchValue":"NOT is:draft (is:inbox OR (is:sent AND to:me))","order":1,"isDefault":true},{"id":"Personal","name":"Personal","searchValue":"is:personal NOT is:sent NOT is:draft","order":2,"isDefault":false},{"id":"Promotions","name":"Promotions","searchValue":"is:promotions NOT is:sent NOT is:draft","order":3,"isDefault":false},{"id":"Updates","name":"Updates","searchValue":"is:updates NOT is:sent NOT is:draft","order":4,"isDefault":false},{"id":"Unread","name":"Unread","searchValue":"is:unread NOT is:sent NOT is:draft","order":5,"isDefault":false}]}'::jsonb;
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/0031_legal_colleen_wing.sql b/apps/server/src/db/migrations/0031_legal_colleen_wing.sql
new file mode 100644
index 0000000000..a25b7f7ce1
--- /dev/null
+++ b/apps/server/src/db/migrations/0031_legal_colleen_wing.sql
@@ -0,0 +1,40 @@
+CREATE TABLE "mail0_oauth_access_token" (
+ "id" text PRIMARY KEY NOT NULL,
+ "access_token" text,
+ "refresh_token" text,
+ "access_token_expires_at" timestamp,
+ "refresh_token_expires_at" timestamp,
+ "client_id" text,
+ "user_id" text,
+ "scopes" text,
+ "created_at" timestamp,
+ "updated_at" timestamp,
+ CONSTRAINT "mail0_oauth_access_token_access_token_unique" UNIQUE("access_token"),
+ CONSTRAINT "mail0_oauth_access_token_refresh_token_unique" UNIQUE("refresh_token")
+);
+--> statement-breakpoint
+CREATE TABLE "mail0_oauth_application" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text,
+ "icon" text,
+ "metadata" text,
+ "client_id" text,
+ "client_secret" text,
+ "redirect_u_r_ls" text,
+ "type" text,
+ "disabled" boolean,
+ "user_id" text,
+ "created_at" timestamp,
+ "updated_at" timestamp,
+ CONSTRAINT "mail0_oauth_application_client_id_unique" UNIQUE("client_id")
+);
+--> statement-breakpoint
+CREATE TABLE "mail0_oauth_consent" (
+ "id" text PRIMARY KEY NOT NULL,
+ "client_id" text,
+ "user_id" text,
+ "scopes" text,
+ "created_at" timestamp,
+ "updated_at" timestamp,
+ "consent_given" boolean
+);
diff --git a/apps/server/src/db/migrations/0032_smiling_raider.sql b/apps/server/src/db/migrations/0032_smiling_raider.sql
new file mode 100644
index 0000000000..cf986c55bb
--- /dev/null
+++ b/apps/server/src/db/migrations/0032_smiling_raider.sql
@@ -0,0 +1 @@
+ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"autoRead":true,"defaultEmailAlias":"","categories":[{"id":"Important","name":"Important","searchValue":"is:important NOT is:sent NOT is:draft","order":0,"isDefault":false},{"id":"All Mail","name":"All Mail","searchValue":"NOT is:draft (is:inbox OR (is:sent AND to:me))","order":1,"isDefault":true},{"id":"Personal","name":"Personal","searchValue":"is:personal NOT is:sent NOT is:draft","order":2,"isDefault":false},{"id":"Promotions","name":"Promotions","searchValue":"is:promotions NOT is:sent NOT is:draft","order":3,"isDefault":false},{"id":"Updates","name":"Updates","searchValue":"is:updates NOT is:sent NOT is:draft","order":4,"isDefault":false},{"id":"Unread","name":"Unread","searchValue":"is:unread NOT is:sent NOT is:draft","order":5,"isDefault":false}]}'::jsonb;
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/0033_first_bastion.sql b/apps/server/src/db/migrations/0033_first_bastion.sql
new file mode 100644
index 0000000000..db2b03d1df
--- /dev/null
+++ b/apps/server/src/db/migrations/0033_first_bastion.sql
@@ -0,0 +1 @@
+ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"autoRead":true,"defaultEmailAlias":"","categories":[{"id":"Important","name":"Important","searchValue":"is:important NOT is:sent NOT is:draft","order":0,"icon":"Lightning","isDefault":false},{"id":"All Mail","name":"All Mail","searchValue":"NOT is:draft (is:inbox OR (is:sent AND to:me))","order":1,"icon":"Mail","isDefault":true},{"id":"Personal","name":"Personal","searchValue":"is:personal NOT is:sent NOT is:draft","order":2,"icon":"User","isDefault":false},{"id":"Promotions","name":"Promotions","searchValue":"is:promotions NOT is:sent NOT is:draft","order":3,"icon":"Tag","isDefault":false},{"id":"Updates","name":"Updates","searchValue":"is:updates NOT is:sent NOT is:draft","order":4,"icon":"Bell","isDefault":false},{"id":"Unread","name":"Unread","searchValue":"is:unread NOT is:sent NOT is:draft","order":5,"icon":"ScanEye","isDefault":false}]}'::jsonb;
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/meta/0029_snapshot.json b/apps/server/src/db/migrations/meta/0029_snapshot.json
index aa70c82369..7be864a515 100644
--- a/apps/server/src/db/migrations/meta/0029_snapshot.json
+++ b/apps/server/src/db/migrations/meta/0029_snapshot.json
@@ -93,12 +93,8 @@
"name": "mail0_account_user_id_mail0_user_id_fk",
"tableFrom": "mail0_account",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -192,12 +188,8 @@
"name": "mail0_connection_user_id_mail0_user_id_fk",
"tableFrom": "mail0_connection",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -207,10 +199,7 @@
"mail0_connection_user_id_email_unique": {
"name": "mail0_connection_user_id_email_unique",
"nullsNotDistinct": false,
- "columns": [
- "user_id",
- "email"
- ]
+ "columns": ["user_id", "email"]
}
},
"policies": {},
@@ -267,9 +256,7 @@
"mail0_early_access_email_unique": {
"name": "mail0_early_access_email_unique",
"nullsNotDistinct": false,
- "columns": [
- "email"
- ]
+ "columns": ["email"]
}
},
"policies": {},
@@ -383,12 +370,8 @@
"name": "mail0_note_user_id_mail0_user_id_fk",
"tableFrom": "mail0_note",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -458,12 +441,8 @@
"name": "mail0_session_user_id_mail0_user_id_fk",
"tableFrom": "mail0_session",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -473,9 +452,7 @@
"mail0_session_token_unique": {
"name": "mail0_session_token_unique",
"nullsNotDistinct": false,
- "columns": [
- "token"
- ]
+ "columns": ["token"]
}
},
"policies": {},
@@ -622,16 +599,12 @@
"mail0_user_email_unique": {
"name": "mail0_user_email_unique",
"nullsNotDistinct": false,
- "columns": [
- "email"
- ]
+ "columns": ["email"]
},
"mail0_user_phone_number_unique": {
"name": "mail0_user_phone_number_unique",
"nullsNotDistinct": false,
- "columns": [
- "phone_number"
- ]
+ "columns": ["phone_number"]
}
},
"policies": {},
@@ -673,12 +646,8 @@
"name": "mail0_user_hotkeys_user_id_mail0_user_id_fk",
"tableFrom": "mail0_user_hotkeys",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -731,12 +700,8 @@
"name": "mail0_user_settings_user_id_mail0_user_id_fk",
"tableFrom": "mail0_user_settings",
"tableTo": "mail0_user",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -746,9 +711,7 @@
"mail0_user_settings_user_id_unique": {
"name": "mail0_user_settings_user_id_unique",
"nullsNotDistinct": false,
- "columns": [
- "user_id"
- ]
+ "columns": ["user_id"]
}
},
"policies": {},
@@ -840,12 +803,8 @@
"name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk",
"tableFrom": "mail0_writing_style_matrix",
"tableTo": "mail0_connection",
- "columnsFrom": [
- "connectionId"
- ],
- "columnsTo": [
- "id"
- ],
+ "columnsFrom": ["connectionId"],
+ "columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -853,9 +812,7 @@
"compositePrimaryKeys": {
"mail0_writing_style_matrix_connectionId_pk": {
"name": "mail0_writing_style_matrix_connectionId_pk",
- "columns": [
- "connectionId"
- ]
+ "columns": ["connectionId"]
}
},
"uniqueConstraints": {},
@@ -875,4 +832,4 @@
"schemas": {},
"tables": {}
}
-}
\ No newline at end of file
+}
diff --git a/apps/server/src/db/migrations/meta/0030_snapshot.json b/apps/server/src/db/migrations/meta/0030_snapshot.json
new file mode 100644
index 0000000000..555e64a2e9
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0030_snapshot.json
@@ -0,0 +1,878 @@
+{
+ "id": "2665d18f-0aa5-48f1-aa41-f4895d385def",
+ "prevId": "d890f4a8-0a7d-4902-8c1b-f58fb6f10e25",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.mail0_account": {
+ "name": "mail0_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_account_user_id_mail0_user_id_fk": {
+ "name": "mail0_account_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_account",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_connection": {
+ "name": "mail0_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "picture": {
+ "name": "picture",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_connection_user_id_mail0_user_id_fk": {
+ "name": "mail0_connection_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_connection",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_connection_user_id_email_unique": {
+ "name": "mail0_connection_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_early_access": {
+ "name": "mail0_early_access",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_early_access": {
+ "name": "is_early_access",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "has_used_ticket": {
+ "name": "has_used_ticket",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_early_access_email_unique": {
+ "name": "mail0_early_access_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_jwks": {
+ "name": "mail0_jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_note": {
+ "name": "mail0_note",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_note_user_id_mail0_user_id_fk": {
+ "name": "mail0_note_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_note",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_session": {
+ "name": "mail0_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_session_user_id_mail0_user_id_fk": {
+ "name": "mail0_session_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_session",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_session_token_unique": {
+ "name": "mail0_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_summary": {
+ "name": "mail0_summary",
+ "schema": "",
+ "columns": {
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "saved": {
+ "name": "saved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suggested_reply": {
+ "name": "suggested_reply",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user": {
+ "name": "mail0_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_connection_id": {
+ "name": "default_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_prompt": {
+ "name": "custom_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number_verified": {
+ "name": "phone_number_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_email_unique": {
+ "name": "mail0_user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "mail0_user_phone_number_unique": {
+ "name": "mail0_user_phone_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "phone_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_hotkeys": {
+ "name": "mail0_user_hotkeys",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "shortcuts": {
+ "name": "shortcuts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_hotkeys_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_hotkeys",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_settings": {
+ "name": "mail0_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"isDefault\":false}]}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_settings_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_settings_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_settings",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_settings_user_id_unique": {
+ "name": "mail0_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_verification": {
+ "name": "mail0_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_writing_style_matrix": {
+ "name": "mail0_writing_style_matrix",
+ "schema": "",
+ "columns": {
+ "connectionId": {
+ "name": "connectionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "numMessages": {
+ "name": "numMessages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "style": {
+ "name": "style",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": {
+ "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk",
+ "tableFrom": "mail0_writing_style_matrix",
+ "tableTo": "mail0_connection",
+ "columnsFrom": [
+ "connectionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mail0_writing_style_matrix_connectionId_pk": {
+ "name": "mail0_writing_style_matrix_connectionId_pk",
+ "columns": [
+ "connectionId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/meta/0031_snapshot.json b/apps/server/src/db/migrations/meta/0031_snapshot.json
new file mode 100644
index 0000000000..d7b9320125
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0031_snapshot.json
@@ -0,0 +1,1114 @@
+{
+ "id": "b111bbbe-f6fe-45b5-8265-c535fa34a077",
+ "prevId": "2665d18f-0aa5-48f1-aa41-f4895d385def",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.mail0_account": {
+ "name": "mail0_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_account_user_id_mail0_user_id_fk": {
+ "name": "mail0_account_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_account",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_connection": {
+ "name": "mail0_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "picture": {
+ "name": "picture",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_connection_user_id_mail0_user_id_fk": {
+ "name": "mail0_connection_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_connection",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_connection_user_id_email_unique": {
+ "name": "mail0_connection_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_early_access": {
+ "name": "mail0_early_access",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_early_access": {
+ "name": "is_early_access",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "has_used_ticket": {
+ "name": "has_used_ticket",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_early_access_email_unique": {
+ "name": "mail0_early_access_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_jwks": {
+ "name": "mail0_jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_note": {
+ "name": "mail0_note",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_note_user_id_mail0_user_id_fk": {
+ "name": "mail0_note_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_note",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_access_token": {
+ "name": "mail0_oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_access_token_access_token_unique": {
+ "name": "mail0_oauth_access_token_access_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "access_token"
+ ]
+ },
+ "mail0_oauth_access_token_refresh_token_unique": {
+ "name": "mail0_oauth_access_token_refresh_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "refresh_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_application": {
+ "name": "mail0_oauth_application",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_u_r_ls": {
+ "name": "redirect_u_r_ls",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_application_client_id_unique": {
+ "name": "mail0_oauth_application_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "client_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_consent": {
+ "name": "mail0_oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consent_given": {
+ "name": "consent_given",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_session": {
+ "name": "mail0_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_session_user_id_mail0_user_id_fk": {
+ "name": "mail0_session_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_session",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_session_token_unique": {
+ "name": "mail0_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_summary": {
+ "name": "mail0_summary",
+ "schema": "",
+ "columns": {
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "saved": {
+ "name": "saved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suggested_reply": {
+ "name": "suggested_reply",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user": {
+ "name": "mail0_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_connection_id": {
+ "name": "default_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_prompt": {
+ "name": "custom_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number_verified": {
+ "name": "phone_number_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_email_unique": {
+ "name": "mail0_user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "mail0_user_phone_number_unique": {
+ "name": "mail0_user_phone_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "phone_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_hotkeys": {
+ "name": "mail0_user_hotkeys",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "shortcuts": {
+ "name": "shortcuts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_hotkeys_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_hotkeys",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_settings": {
+ "name": "mail0_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"isDefault\":false}]}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_settings_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_settings_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_settings",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_settings_user_id_unique": {
+ "name": "mail0_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_verification": {
+ "name": "mail0_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_writing_style_matrix": {
+ "name": "mail0_writing_style_matrix",
+ "schema": "",
+ "columns": {
+ "connectionId": {
+ "name": "connectionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "numMessages": {
+ "name": "numMessages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "style": {
+ "name": "style",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": {
+ "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk",
+ "tableFrom": "mail0_writing_style_matrix",
+ "tableTo": "mail0_connection",
+ "columnsFrom": [
+ "connectionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mail0_writing_style_matrix_connectionId_pk": {
+ "name": "mail0_writing_style_matrix_connectionId_pk",
+ "columns": [
+ "connectionId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/meta/0032_snapshot.json b/apps/server/src/db/migrations/meta/0032_snapshot.json
new file mode 100644
index 0000000000..514bdcbbe1
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0032_snapshot.json
@@ -0,0 +1,1114 @@
+{
+ "id": "dc914d1f-e63e-4d9b-b47e-437c41235b45",
+ "prevId": "b111bbbe-f6fe-45b5-8265-c535fa34a077",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.mail0_account": {
+ "name": "mail0_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_account_user_id_mail0_user_id_fk": {
+ "name": "mail0_account_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_account",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_connection": {
+ "name": "mail0_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "picture": {
+ "name": "picture",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_connection_user_id_mail0_user_id_fk": {
+ "name": "mail0_connection_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_connection",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_connection_user_id_email_unique": {
+ "name": "mail0_connection_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_early_access": {
+ "name": "mail0_early_access",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_early_access": {
+ "name": "is_early_access",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "has_used_ticket": {
+ "name": "has_used_ticket",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_early_access_email_unique": {
+ "name": "mail0_early_access_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_jwks": {
+ "name": "mail0_jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_note": {
+ "name": "mail0_note",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_note_user_id_mail0_user_id_fk": {
+ "name": "mail0_note_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_note",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_access_token": {
+ "name": "mail0_oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_access_token_access_token_unique": {
+ "name": "mail0_oauth_access_token_access_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "access_token"
+ ]
+ },
+ "mail0_oauth_access_token_refresh_token_unique": {
+ "name": "mail0_oauth_access_token_refresh_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "refresh_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_application": {
+ "name": "mail0_oauth_application",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_u_r_ls": {
+ "name": "redirect_u_r_ls",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_application_client_id_unique": {
+ "name": "mail0_oauth_application_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "client_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_consent": {
+ "name": "mail0_oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consent_given": {
+ "name": "consent_given",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_session": {
+ "name": "mail0_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_session_user_id_mail0_user_id_fk": {
+ "name": "mail0_session_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_session",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_session_token_unique": {
+ "name": "mail0_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_summary": {
+ "name": "mail0_summary",
+ "schema": "",
+ "columns": {
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "saved": {
+ "name": "saved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suggested_reply": {
+ "name": "suggested_reply",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user": {
+ "name": "mail0_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_connection_id": {
+ "name": "default_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_prompt": {
+ "name": "custom_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number_verified": {
+ "name": "phone_number_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_email_unique": {
+ "name": "mail0_user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "mail0_user_phone_number_unique": {
+ "name": "mail0_user_phone_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "phone_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_hotkeys": {
+ "name": "mail0_user_hotkeys",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "shortcuts": {
+ "name": "shortcuts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_hotkeys_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_hotkeys",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_settings": {
+ "name": "mail0_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"autoRead\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"isDefault\":false}]}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_settings_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_settings_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_settings",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_settings_user_id_unique": {
+ "name": "mail0_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_verification": {
+ "name": "mail0_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_writing_style_matrix": {
+ "name": "mail0_writing_style_matrix",
+ "schema": "",
+ "columns": {
+ "connectionId": {
+ "name": "connectionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "numMessages": {
+ "name": "numMessages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "style": {
+ "name": "style",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": {
+ "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk",
+ "tableFrom": "mail0_writing_style_matrix",
+ "tableTo": "mail0_connection",
+ "columnsFrom": [
+ "connectionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mail0_writing_style_matrix_connectionId_pk": {
+ "name": "mail0_writing_style_matrix_connectionId_pk",
+ "columns": [
+ "connectionId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/meta/0033_snapshot.json b/apps/server/src/db/migrations/meta/0033_snapshot.json
new file mode 100644
index 0000000000..5c5394476f
--- /dev/null
+++ b/apps/server/src/db/migrations/meta/0033_snapshot.json
@@ -0,0 +1,1114 @@
+{
+ "id": "63675907-78bc-4b07-a565-3fdf2e24f8b9",
+ "prevId": "dc914d1f-e63e-4d9b-b47e-437c41235b45",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.mail0_account": {
+ "name": "mail0_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_account_user_id_mail0_user_id_fk": {
+ "name": "mail0_account_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_account",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_connection": {
+ "name": "mail0_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "picture": {
+ "name": "picture",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_connection_user_id_mail0_user_id_fk": {
+ "name": "mail0_connection_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_connection",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_connection_user_id_email_unique": {
+ "name": "mail0_connection_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_early_access": {
+ "name": "mail0_early_access",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_early_access": {
+ "name": "is_early_access",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "has_used_ticket": {
+ "name": "has_used_ticket",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_early_access_email_unique": {
+ "name": "mail0_early_access_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_jwks": {
+ "name": "mail0_jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_note": {
+ "name": "mail0_note",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_note_user_id_mail0_user_id_fk": {
+ "name": "mail0_note_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_note",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_access_token": {
+ "name": "mail0_oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_access_token_access_token_unique": {
+ "name": "mail0_oauth_access_token_access_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "access_token"
+ ]
+ },
+ "mail0_oauth_access_token_refresh_token_unique": {
+ "name": "mail0_oauth_access_token_refresh_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "refresh_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_application": {
+ "name": "mail0_oauth_application",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_u_r_ls": {
+ "name": "redirect_u_r_ls",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_oauth_application_client_id_unique": {
+ "name": "mail0_oauth_application_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "client_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_oauth_consent": {
+ "name": "mail0_oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consent_given": {
+ "name": "consent_given",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_session": {
+ "name": "mail0_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_session_user_id_mail0_user_id_fk": {
+ "name": "mail0_session_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_session",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_session_token_unique": {
+ "name": "mail0_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_summary": {
+ "name": "mail0_summary",
+ "schema": "",
+ "columns": {
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "saved": {
+ "name": "saved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suggested_reply": {
+ "name": "suggested_reply",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user": {
+ "name": "mail0_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_connection_id": {
+ "name": "default_connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_prompt": {
+ "name": "custom_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone_number_verified": {
+ "name": "phone_number_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_email_unique": {
+ "name": "mail0_user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "mail0_user_phone_number_unique": {
+ "name": "mail0_user_phone_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "phone_number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_hotkeys": {
+ "name": "mail0_user_hotkeys",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "shortcuts": {
+ "name": "shortcuts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_hotkeys_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_hotkeys",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_user_settings": {
+ "name": "mail0_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"autoRead\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"icon\":\"Lightning\",\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"icon\":\"Mail\",\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"icon\":\"User\",\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"icon\":\"Tag\",\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"icon\":\"Bell\",\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"icon\":\"ScanEye\",\"isDefault\":false}]}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_user_settings_user_id_mail0_user_id_fk": {
+ "name": "mail0_user_settings_user_id_mail0_user_id_fk",
+ "tableFrom": "mail0_user_settings",
+ "tableTo": "mail0_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mail0_user_settings_user_id_unique": {
+ "name": "mail0_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_verification": {
+ "name": "mail0_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mail0_writing_style_matrix": {
+ "name": "mail0_writing_style_matrix",
+ "schema": "",
+ "columns": {
+ "connectionId": {
+ "name": "connectionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "numMessages": {
+ "name": "numMessages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "style": {
+ "name": "style",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": {
+ "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk",
+ "tableFrom": "mail0_writing_style_matrix",
+ "tableTo": "mail0_connection",
+ "columnsFrom": [
+ "connectionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mail0_writing_style_matrix_connectionId_pk": {
+ "name": "mail0_writing_style_matrix_connectionId_pk",
+ "columns": [
+ "connectionId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json
index f062fe0f03..82a4834b85 100644
--- a/apps/server/src/db/migrations/meta/_journal.json
+++ b/apps/server/src/db/migrations/meta/_journal.json
@@ -211,6 +211,34 @@
"when": 1750268247218,
"tag": "0029_common_network",
"breakpoints": true
+ },
+ {
+ "idx": 30,
+ "version": "7",
+ "when": 1750384854243,
+ "tag": "0030_blue_grandmaster",
+ "breakpoints": true
+ },
+ {
+ "idx": 31,
+ "version": "7",
+ "when": 1750551357064,
+ "tag": "0031_legal_colleen_wing",
+ "breakpoints": true
+ },
+ {
+ "idx": 32,
+ "version": "7",
+ "when": 1750794533968,
+ "tag": "0032_smiling_raider",
+ "breakpoints": true
+ },
+ {
+ "idx": 33,
+ "version": "7",
+ "when": 1750885796261,
+ "tag": "0033_first_bastion",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts
index 8751a48f4f..ffdd57a967 100644
--- a/apps/server/src/db/schema.ts
+++ b/apps/server/src/db/schema.ts
@@ -172,3 +172,41 @@ export const jwks = createTable('jwks', {
privateKey: text('private_key').notNull(),
createdAt: timestamp('created_at').notNull(),
});
+
+export const oauthApplication = createTable('oauth_application', {
+ id: text('id').primaryKey(),
+ name: text('name'),
+ icon: text('icon'),
+ metadata: text('metadata'),
+ clientId: text('client_id').unique(),
+ clientSecret: text('client_secret'),
+ redirectURLs: text('redirect_u_r_ls'),
+ type: text('type'),
+ disabled: boolean('disabled'),
+ userId: text('user_id'),
+ createdAt: timestamp('created_at'),
+ updatedAt: timestamp('updated_at'),
+});
+
+export const oauthAccessToken = createTable('oauth_access_token', {
+ id: text('id').primaryKey(),
+ accessToken: text('access_token').unique(),
+ refreshToken: text('refresh_token').unique(),
+ accessTokenExpiresAt: timestamp('access_token_expires_at'),
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
+ clientId: text('client_id'),
+ userId: text('user_id'),
+ scopes: text('scopes'),
+ createdAt: timestamp('created_at'),
+ updatedAt: timestamp('updated_at'),
+});
+
+export const oauthConsent = createTable('oauth_consent', {
+ id: text('id').primaryKey(),
+ clientId: text('client_id'),
+ userId: text('user_id'),
+ scopes: text('scopes'),
+ createdAt: timestamp('created_at'),
+ updatedAt: timestamp('updated_at'),
+ consentGiven: boolean('consent_given'),
+});
diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts
index 68b5004665..f30cd9ab2e 100644
--- a/apps/server/src/lib/auth.ts
+++ b/apps/server/src/lib/auth.ts
@@ -6,7 +6,7 @@ import {
session,
userHotkeys,
} from '../db/schema';
-import { createAuthMiddleware, phoneNumber, jwt, bearer } from 'better-auth/plugins';
+import { createAuthMiddleware, phoneNumber, jwt, bearer, mcp } from 'better-auth/plugins';
import { type Account, betterAuth, type BetterAuthOptions } from 'better-auth';
import { getBrowserTimezone, isValidTimezone } from './timezones';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
@@ -14,6 +14,7 @@ import { getSocialProviders } from './auth-providers';
import { redis, resend, twilio } from './services';
import { getContext } from 'hono/context-storage';
import { defaultUserSettings } from './schemas';
+import { getMigrations } from 'better-auth/db';
import { disableBrainFunction } from './brain';
import { APIError } from 'better-auth/api';
import { getZeroDB } from './server-utils';
@@ -61,7 +62,6 @@ const connectionHandlerHook = async (account: Account) => {
const [result] = await db.createConnection(
account.providerId as EProviders,
userInfo.address,
- account.userId,
updatingInfo,
);
@@ -78,6 +78,9 @@ export const createAuth = () => {
return betterAuth({
plugins: [
+ mcp({
+ loginPage: env.VITE_PUBLIC_APP_URL + '/login',
+ }),
jwt(),
bearer(),
phoneNumber({
@@ -113,7 +116,7 @@ export const createAuth = () => {
beforeDelete: async (user, request) => {
if (!request) throw new APIError('BAD_REQUEST', { message: 'Request object is missing' });
const db = getZeroDB(user.id);
- const connections = await db.findManyConnections(user.id);
+ const connections = await db.findManyConnections();
const revokedAccounts = (
await Promise.allSettled(
@@ -146,7 +149,7 @@ export const createAuth = () => {
console.log('Failed to revoke some accounts');
}
- await db.deleteUser(user.id);
+ await db.deleteUser();
},
},
},
@@ -204,7 +207,7 @@ export const createAuth = () => {
if (newSession) {
// Check if user already has settings
const db = getZeroDB(newSession.user.id);
- const existingSettings = await db.findUserSettings(newSession.user.id);
+ const existingSettings = await db.findUserSettings();
if (!existingSettings) {
// get timezone from vercel's header
@@ -215,7 +218,7 @@ export const createAuth = () => {
? headerTimezone
: getBrowserTimezone();
// write default settings against the user
- await db.insertUserSettings(newSession.user.id, {
+ await db.insertUserSettings({
...defaultUserSettings,
timezone,
});
@@ -230,12 +233,13 @@ export const createAuth = () => {
const createAuthConfig = () => {
const cache = redis();
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
return {
database: drizzleAdapter(db, { provider: 'pg' }),
secondaryStorage: {
get: async (key: string) => {
- return ((await cache.get(key)) as string) ?? null;
+ const value = await cache.get(key);
+ return typeof value === 'string' ? value : value ? JSON.stringify(value) : null;
},
set: async (key: string, value: string, ttl?: number) => {
if (ttl) await cache.set(key, value, { ex: ttl });
diff --git a/apps/server/src/lib/brain.ts b/apps/server/src/lib/brain.ts
index 24fe696fcf..678ea18e6a 100644
--- a/apps/server/src/lib/brain.ts
+++ b/apps/server/src/lib/brain.ts
@@ -1,4 +1,5 @@
import { ReSummarizeThread, SummarizeMessage, SummarizeThread } from './brain.fallback.prompts';
+import { AiChatPrompt, StyledEmailAssistantSystemPrompt } from './prompts';
import { getSubscriptionFactory } from './factories/subscription-factory.registry';
import { EPrompts, EProviders } from '../types';
import { env } from 'cloudflare:workers';
@@ -29,7 +30,7 @@ const getPromptName = (connectionId: string, prompt: EPrompts) => {
export const getPrompt = async (promptName: string, fallback: string) => {
const existingPrompt = await env.prompts_storage.get(promptName);
- if (!existingPrompt) {
+ if (!existingPrompt || existingPrompt === 'undefined') {
await env.prompts_storage.put(promptName, fallback);
return fallback;
}
@@ -41,15 +42,17 @@ export const getPrompts = async ({ connectionId }: { connectionId: string }) =>
[EPrompts.SummarizeMessage]: '',
[EPrompts.ReSummarizeThread]: '',
[EPrompts.SummarizeThread]: '',
+ [EPrompts.Chat]: '',
+ [EPrompts.Compose]: '',
// [EPrompts.ThreadLabels]: '',
- // [EPrompts.Chat]: '',
};
const fallbackPrompts = {
[EPrompts.SummarizeMessage]: SummarizeMessage,
[EPrompts.ReSummarizeThread]: ReSummarizeThread,
[EPrompts.SummarizeThread]: SummarizeThread,
+ [EPrompts.Chat]: AiChatPrompt('', '', ''),
+ [EPrompts.Compose]: StyledEmailAssistantSystemPrompt(),
// [EPrompts.ThreadLabels]: '',
- // [EPrompts.Chat]: '',
};
for (const promptType of Object.values(EPrompts)) {
const promptName = getPromptName(connectionId, promptType);
diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts
index 124ad7e776..38443d5e0a 100644
--- a/apps/server/src/lib/driver/google.ts
+++ b/apps/server/src/lib/driver/google.ts
@@ -82,30 +82,22 @@ export class GoogleMailManager implements MailManager {
}
public getEmailAliases() {
return this.withErrorHandler('getEmailAliases', async () => {
- console.log('Fetching email aliases...');
-
const profile = await this.gmail.users.getProfile({
userId: 'me',
});
- console.log('Retrieved user profile:', { email: profile.data.emailAddress });
const primaryEmail = profile.data.emailAddress || '';
const aliases: { email: string; name?: string; primary?: boolean }[] = [
{ email: primaryEmail, primary: true },
];
- console.log('Added primary email to aliases:', { primaryEmail });
const settings = await this.gmail.users.settings.sendAs.list({
userId: 'me',
});
- console.log('Retrieved sendAs settings:', {
- sendAsCount: settings.data.sendAs?.length || 0,
- });
if (settings.data.sendAs) {
settings.data.sendAs.forEach((alias) => {
if (alias.isPrimary && alias.sendAsEmail === primaryEmail) {
- console.log('Skipping duplicate primary email:', { email: alias.sendAsEmail });
return;
}
@@ -114,15 +106,9 @@ export class GoogleMailManager implements MailManager {
name: alias.displayName || undefined,
primary: alias.isPrimary || false,
});
- console.log('Added alias:', {
- email: alias.sendAsEmail,
- name: alias.displayName,
- primary: alias.isPrimary,
- });
});
}
- console.log('Returning aliases:', { aliasCount: aliases.length });
return aliases;
});
}
@@ -569,7 +555,7 @@ export class GoogleMailManager implements MailManager {
return this.withErrorHandler(
'createDraft',
async () => {
- const message = await sanitizeTipTapHtml(data.message);
+ const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
const msg = createMimeMessage();
msg.setSender('me');
// name
@@ -593,10 +579,24 @@ export class GoogleMailManager implements MailManager {
data: message || '',
});
+ if (inlineImages.length > 0) {
+ for (const image of inlineImages) {
+ msg.addAttachment({
+ inline: true,
+ filename: `${image.cid}`,
+ contentType: image.mimeType,
+ data: image.data,
+ headers: {
+ 'Content-ID': `<${image.cid}>`,
+ 'Content-Disposition': 'inline',
+ },
+ });
+ }
+ }
+
if (data.attachments && data.attachments?.length > 0) {
for (const attachment of data.attachments) {
- const arrayBuffer = await attachment.arrayBuffer();
- const base64Data = Buffer.from(arrayBuffer).toString('base64');
+ const base64Data = attachment.base64;
msg.addAttachment({
filename: attachment.name,
contentType: attachment.type,
@@ -1039,18 +1039,35 @@ export class GoogleMailManager implements MailManager {
msg.setSubject(subject);
+ const { html: processedMessage, inlineImages } = await sanitizeTipTapHtml(message.trim());
+
if (originalMessage) {
msg.addMessage({
contentType: 'text/html',
- data: `${await sanitizeTipTapHtml(message.trim())}${originalMessage}`,
+ data: `${processedMessage}${originalMessage}`,
});
} else {
msg.addMessage({
contentType: 'text/html',
- data: await sanitizeTipTapHtml(message.trim()),
+ data: processedMessage,
});
}
+ if (inlineImages.length > 0) {
+ for (const image of inlineImages) {
+ msg.addAttachment({
+ inline: true,
+ filename: `${image.cid}`,
+ contentType: image.mimeType,
+ data: image.data,
+ headers: {
+ 'Content-ID': `<${image.cid}>`,
+ 'Content-Disposition': 'inline',
+ },
+ });
+ }
+ }
+
if (headers) {
Object.entries(headers).forEach(([key, value]) => {
if (value) {
@@ -1073,9 +1090,7 @@ export class GoogleMailManager implements MailManager {
if (attachments?.length > 0) {
for (const file of attachments) {
- const arrayBuffer = await file.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- const base64Content = buffer.toString('base64');
+ const base64Content = file.base64;
msg.addAttachment({
filename: file.name,
@@ -1118,7 +1133,10 @@ export class GoogleMailManager implements MailManager {
}
}
- // TODO: Hook up CC and BCC from the draft so it can populate the composer on open.
+ const cc =
+ draft.message.payload?.headers?.find((h) => h.name === 'Cc')?.value?.split(',') || [];
+ const bcc =
+ draft.message.payload?.headers?.find((h) => h.name === 'Bcc')?.value?.split(',') || [];
return {
id: draft.id || '',
@@ -1126,6 +1144,8 @@ export class GoogleMailManager implements MailManager {
subject: subject ? he.decode(subject).trim() : '',
content,
rawMessage: draft.message,
+ cc,
+ bcc,
};
}
private async withErrorHandler(
diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts
index ab5ec156dc..b6c3b857e6 100644
--- a/apps/server/src/lib/driver/microsoft.ts
+++ b/apps/server/src/lib/driver/microsoft.ts
@@ -633,7 +633,7 @@ export class OutlookMailManager implements MailManager {
return this.withErrorHandler(
'createDraft',
async () => {
- const message = await sanitizeTipTapHtml(data.message);
+ const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', ');
@@ -671,8 +671,23 @@ export class OutlookMailManager implements MailManager {
}));
}
+ const allAttachments = [];
+
+ if (inlineImages.length > 0) {
+ for (const image of inlineImages) {
+ allAttachments.push({
+ '@odata.type': '#microsoft.graph.fileAttachment',
+ name: image.cid,
+ contentType: image.mimeType,
+ contentBytes: image.data,
+ contentId: image.cid,
+ isInline: true,
+ });
+ }
+ }
+
if (data.attachments && data.attachments.length > 0) {
- outlookMessage.attachments = await Promise.all(
+ const regularAttachments = await Promise.all(
data.attachments.map(async (file) => {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
@@ -686,6 +701,11 @@ export class OutlookMailManager implements MailManager {
};
}),
);
+ allAttachments.push(...regularAttachments);
+ }
+
+ if (allAttachments.length > 0) {
+ outlookMessage.attachments = allAttachments;
}
let res;
@@ -1103,11 +1123,12 @@ export class OutlookMailManager implements MailManager {
}: IOutgoingMessage): Promise {
// Outlook Graph API expects a Message object structure for sending/creating drafts
console.log(to);
+ const { html: processedMessage, inlineImages } = await sanitizeTipTapHtml(message.trim());
const outlookMessage: Message = {
subject: subject,
body: {
contentType: 'html', // Or 'text'
- content: await sanitizeTipTapHtml(message.trim()),
+ content: processedMessage,
},
toRecipients:
to?.map((rec) => ({
@@ -1151,23 +1172,42 @@ export class OutlookMailManager implements MailManager {
// outlookMessage.references = headers.references as string | undefined; // Example if supported
}
- if (attachments && attachments.length > 0) {
- outlookMessage.attachments = await Promise.all(
+ // Handle inline images and attachments
+ const allAttachments = [];
+
+ if (inlineImages.length > 0) {
+ for (const image of inlineImages) {
+ allAttachments.push({
+ '@odata.type': '#microsoft.graph.fileAttachment',
+ name: image.cid,
+ contentType: image.mimeType,
+ contentBytes: image.data,
+ contentId: image.cid,
+ isInline: true,
+ });
+ }
+ }
+
+ if (attachments?.length > 0) {
+ const regularAttachments = await Promise.all(
attachments.map(async (file) => {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const base64Content = buffer.toString('base64');
- // Graph API expects a FileAttachment object or ItemAttachment object
- // Assuming FileAttachment for typical file uploads
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: file.name,
contentType: file.type || 'application/octet-stream',
- contentBytes: base64Content, // Base64 content here
+ contentBytes: base64Content,
};
}),
);
+ allAttachments.push(...regularAttachments);
+ }
+
+ if (allAttachments.length > 0) {
+ outlookMessage.attachments = allAttachments;
}
return outlookMessage;
diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts
index 7cd8650f92..806cc76556 100644
--- a/apps/server/src/lib/driver/types.ts
+++ b/apps/server/src/lib/driver/types.ts
@@ -16,6 +16,8 @@ export interface ParsedDraft {
subject?: string;
content?: string;
rawMessage?: T;
+ cc?: string[];
+ bcc?: string[];
}
export interface IConfig {
diff --git a/apps/server/src/lib/driver/utils.ts b/apps/server/src/lib/driver/utils.ts
index 0d2b9f9853..2e54384831 100644
--- a/apps/server/src/lib/driver/utils.ts
+++ b/apps/server/src/lib/driver/utils.ts
@@ -16,7 +16,7 @@ export const deleteActiveConnection = async () => {
try {
await c.var.auth.api.signOut({ headers: c.req.raw.headers });
const db = getZeroDB(session.user.id);
- await db.deleteActiveConnection(session.user.id, activeConnection.id);
+ await db.deleteActiveConnection(activeConnection.id);
} catch (error) {
console.error('Server: Error deleting connection:', error);
throw error;
diff --git a/apps/server/src/lib/factories/base-subscription.factory.ts b/apps/server/src/lib/factories/base-subscription.factory.ts
index acc327090d..0d0838a38d 100644
--- a/apps/server/src/lib/factories/base-subscription.factory.ts
+++ b/apps/server/src/lib/factories/base-subscription.factory.ts
@@ -4,6 +4,8 @@ import { connection } from '../../db/schema';
import type { HonoContext } from '../../ctx';
import { getZeroDB } from '../server-utils';
import { env } from 'cloudflare:workers';
+import { createDb } from '../../db';
+import { eq } from 'drizzle-orm';
export interface SubscriptionData {
connectionId?: string;
@@ -27,8 +29,11 @@ export abstract class BaseSubscriptionFactory {
protected async getConnectionFromDb(connectionId: string) {
// Revisit
- const db = getZeroDB('global-db');
- const connectionData = await db.findConnectionById(connectionId);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
+ const connectionData = await db.query.connection.findFirst({
+ where: eq(connection.id, connectionId),
+ });
+ await conn.end();
return connectionData;
}
diff --git a/apps/server/src/lib/notes-manager.ts b/apps/server/src/lib/notes-manager.ts
index 2ab493d1e6..49894d820d 100644
--- a/apps/server/src/lib/notes-manager.ts
+++ b/apps/server/src/lib/notes-manager.ts
@@ -6,7 +6,7 @@ export class NotesManager {
async getThreadNotes(userId: string, threadId: string): Promise<(typeof note.$inferSelect)[]> {
const db = getZeroDB(userId);
- return await db.findManyNotesByThreadId(userId, threadId);
+ return await db.findManyNotesByThreadId(threadId);
}
async createNote(
@@ -17,11 +17,10 @@ export class NotesManager {
isPinned: boolean = false,
): Promise {
const db = getZeroDB(userId);
- const highestOrder = await db.findHighestNoteOrder(userId);
+ const highestOrder = await db.findHighestNoteOrder();
const id = crypto.randomUUID();
- const result = await db.createNote(userId, {
- userId,
+ const result = await db.createNote({
id,
threadId,
content,
@@ -44,13 +43,13 @@ export class NotesManager {
>,
): Promise {
const db = getZeroDB(userId);
- const existingNote = await db.findNoteById(userId, noteId);
+ const existingNote = await db.findNoteById(noteId);
if (!existingNote) {
throw new Error('Note not found or unauthorized');
}
- const result = await db.updateNote(userId, noteId, data);
+ const result = await db.updateNote(noteId, data);
if (!result) {
throw new Error('Failed to update note');
@@ -61,7 +60,7 @@ export class NotesManager {
async deleteNote(userId: string, noteId: string) {
const db = getZeroDB(userId);
try {
- await db.deleteNote(userId, noteId);
+ await db.deleteNote(noteId);
return true;
} catch (error) {
console.error('Error deleting note:', error);
@@ -80,7 +79,7 @@ export class NotesManager {
const noteIds = notes.map((n) => n.id);
const db = getZeroDB(userId);
- const userNotes = await db.findManyNotesByIds(userId, noteIds);
+ const userNotes = await db.findManyNotesByIds(noteIds);
const foundNoteIds = new Set(userNotes.map((n) => n.id));
@@ -90,6 +89,6 @@ export class NotesManager {
throw new Error('One or more notes not found or unauthorized');
}
- return await db.updateManyNotes(userId, notes);
+ return await db.updateManyNotes(notes);
}
}
diff --git a/apps/server/src/lib/sanitize-tip-tap-html.ts b/apps/server/src/lib/sanitize-tip-tap-html.ts
index c1f8b7c102..29387531ba 100644
--- a/apps/server/src/lib/sanitize-tip-tap-html.ts
+++ b/apps/server/src/lib/sanitize-tip-tap-html.ts
@@ -1,15 +1,53 @@
import { Html } from '@react-email/components';
import { render } from '@react-email/render';
import sanitizeHtml from 'sanitize-html';
+import { v4 as uuidv4 } from 'uuid';
import React from 'react';
-export const sanitizeTipTapHtml = async (html: string) => {
- const clean = sanitizeHtml(html);
- return render(
+interface InlineImage {
+ cid: string;
+ data: string;
+ mimeType: string;
+}
+
+export const sanitizeTipTapHtml = async (
+ html: string,
+): Promise<{ html: string; inlineImages: InlineImage[] }> => {
+ const inlineImages: InlineImage[] = [];
+
+ const processedHtml = html.replace(
+ / ]+src=["']data:([^;]+);base64,([^"']+)["'][^>]*>/gi,
+ (match, mimeType, base64Data) => {
+ const cid = `image_${uuidv4()}@0.email`;
+ inlineImages.push({
+ cid,
+ data: base64Data,
+ mimeType,
+ });
+
+ return match.replace(/src=["']data:[^"']+["']/i, `src="cid:${cid}"`);
+ },
+ );
+
+ const clean = sanitizeHtml(processedHtml, {
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+ allowedAttributes: {
+ ...sanitizeHtml.defaults.allowedAttributes,
+ img: ['src', 'alt', 'width', 'height', 'style'],
+ },
+ allowedSchemes: ['http', 'https', 'cid', 'data'],
+ });
+
+ const renderedHtml = await render(
React.createElement(
Html,
{},
React.createElement('div', { dangerouslySetInnerHTML: { __html: clean } }),
- ),
+ ) as any,
);
+
+ return {
+ html: renderedHtml,
+ inlineImages,
+ };
};
diff --git a/apps/server/src/lib/schemas.ts b/apps/server/src/lib/schemas.ts
index 0a838040f6..7cd0aab39d 100644
--- a/apps/server/src/lib/schemas.ts
+++ b/apps/server/src/lib/schemas.ts
@@ -28,9 +28,10 @@ export const createDraftData = z.object({
bcc: z.string().optional(),
subject: z.string(),
message: z.string(),
- attachments: z.array(serializedFileSchema).transform(deserializeFiles).optional(),
+ attachments: z.array(serializedFileSchema).optional(),
id: z.string().nullable(),
threadId: z.string().nullable(),
+ fromEmail: z.string().nullable(),
});
export type CreateDraftData = z.infer;
@@ -40,6 +41,7 @@ export const mailCategorySchema = z.object({
name: z.string(),
searchValue: z.string(),
order: z.number().int(),
+ icon: z.string().optional(),
isDefault: z.boolean().optional().default(false),
});
@@ -51,6 +53,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'Important',
searchValue: 'is:important NOT is:sent NOT is:draft',
order: 0,
+ icon: 'Lightning',
isDefault: false,
},
{
@@ -58,6 +61,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'All Mail',
searchValue: 'NOT is:draft (is:inbox OR (is:sent AND to:me))',
order: 1,
+ icon: 'Mail',
isDefault: true,
},
{
@@ -65,6 +69,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'Personal',
searchValue: 'is:personal NOT is:sent NOT is:draft',
order: 2,
+ icon: 'User',
isDefault: false,
},
{
@@ -72,6 +77,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'Promotions',
searchValue: 'is:promotions NOT is:sent NOT is:draft',
order: 3,
+ icon: 'Tag',
isDefault: false,
},
{
@@ -79,6 +85,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'Updates',
searchValue: 'is:updates NOT is:sent NOT is:draft',
order: 4,
+ icon: 'Bell',
isDefault: false,
},
{
@@ -86,6 +93,7 @@ export const defaultMailCategories: MailCategory[] = [
name: 'Unread',
searchValue: 'is:unread NOT is:sent NOT is:draft',
order: 5,
+ icon: 'ScanEye',
isDefault: false,
},
];
@@ -113,12 +121,14 @@ export const userSettingsSchema = z.object({
timezone: z.string(),
dynamicContent: z.boolean().optional(),
externalImages: z.boolean(),
- customPrompt: z.string(),
+ customPrompt: z.string().default(''),
isOnboarded: z.boolean().optional(),
trustedSenders: z.string().array().optional(),
colorTheme: z.enum(['light', 'dark', 'system']).default('system'),
zeroSignature: z.boolean().default(true),
categories: categoriesSchema.optional(),
+ defaultEmailAlias: z.string().optional(),
+ autoRead: z.boolean().default(true),
});
export type UserSettings = z.infer;
@@ -133,5 +143,7 @@ export const defaultUserSettings: UserSettings = {
isOnboarded: false,
colorTheme: 'system',
zeroSignature: true,
+ autoRead: true,
+ defaultEmailAlias: '',
categories: defaultMailCategories,
};
diff --git a/apps/server/src/lib/server-utils.ts b/apps/server/src/lib/server-utils.ts
index 6304e5ea24..fe0121a2cc 100644
--- a/apps/server/src/lib/server-utils.ts
+++ b/apps/server/src/lib/server-utils.ts
@@ -1,10 +1,21 @@
import { getContext } from 'hono/context-storage';
-import { connection, user } from '../db/schema';
+import { connection } from '../db/schema';
import type { HonoContext } from '../ctx';
import { env } from 'cloudflare:workers';
import { createDriver } from './driver';
-export const getZeroDB = (userId: string) => env.ZERO_DB.get(env.ZERO_DB.idFromName(userId));
+export const getZeroDB = (userId: string) => {
+ const stub = env.ZERO_DB.get(env.ZERO_DB.idFromName(userId));
+ const rpcTarget = stub.setMetaData(userId);
+ return rpcTarget;
+};
+
+export const getZeroAgent = async (connectionId: string) => {
+ const stub = env.ZERO_AGENT.get(env.ZERO_AGENT.idFromName(connectionId));
+ const rpcTarget = await stub.setMetaData(connectionId);
+ await rpcTarget.setupAuth(connectionId);
+ return rpcTarget;
+};
export const getActiveConnection = async () => {
const c = getContext();
@@ -13,17 +24,14 @@ export const getActiveConnection = async () => {
const db = getZeroDB(sessionUser.id);
- const userData = await db.findUser(sessionUser.id);
+ const userData = await db.findUser();
if (userData?.defaultConnectionId) {
- const activeConnection = await db.findUserConnection(
- sessionUser.id,
- userData.defaultConnectionId,
- );
+ const activeConnection = await db.findUserConnection(userData.defaultConnectionId);
if (activeConnection) return activeConnection;
}
- const firstConnection = await db.findFirstConnection(sessionUser.id);
+ const firstConnection = await db.findFirstConnection();
if (!firstConnection) {
console.error(`No connections found for user ${sessionUser.id}`);
throw new Error('No connections found for user');
@@ -85,8 +93,7 @@ export const notifyUser = async ({
payload,
});
- const durableObject = env.ZERO_AGENT.idFromName(connectionId);
- const mailbox = env.ZERO_AGENT.get(durableObject);
+ const mailbox = await getZeroAgent(connectionId);
try {
console.log(`[notifyUser] Broadcasting message`, {
diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
index d31698ca54..b596062ede 100644
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -14,15 +14,17 @@ import {
userSettings,
writingStyleMatrix,
} from './db/schema';
-import { env, WorkerEntrypoint, DurableObject } from 'cloudflare:workers';
+import { env, WorkerEntrypoint, DurableObject, RpcTarget } from 'cloudflare:workers';
+import { getZeroAgent, getZeroDB, verifyToken } from './lib/server-utils';
import { MainWorkflow, ThreadWorkflow, ZeroWorkflow } from './pipelines';
-import { getZeroDB, verifyToken } from './lib/server-utils';
+import { oAuthDiscoveryMetadata } from 'better-auth/plugins';
import { EProviders, type ISubscribeBatch } from './types';
import { eq, and, desc, asc, inArray } from 'drizzle-orm';
import { contextStorage } from 'hono/context-storage';
import { defaultUserSettings } from './lib/schemas';
import { createLocalJWKSet, jwtVerify } from 'jose';
import { routePartykitRequest } from 'partyserver';
+import { withMcpAuth } from 'better-auth/plugins';
import { enableBrainFunction } from './lib/brain';
import { trpcServer } from '@hono/trpc-server';
import { agentsMiddleware } from 'hono-agents';
@@ -40,8 +42,143 @@ import { appRouter } from './trpc';
import { cors } from 'hono/cors';
import { Hono } from 'hono';
-class ZeroDB extends DurableObject {
- db: DB = createDb(env.HYPERDRIVE.connectionString);
+export class DbRpcDO extends RpcTarget {
+ constructor(
+ private mainDo: ZeroDB,
+ private userId: string,
+ ) {
+ super();
+ }
+
+ async findUser(): Promise {
+ return await this.mainDo.findUser(this.userId);
+ }
+
+ async findUserConnection(
+ connectionId: string,
+ ): Promise {
+ return await this.mainDo.findUserConnection(this.userId, connectionId);
+ }
+
+ async updateUser(data: Partial) {
+ return await this.mainDo.updateUser(this.userId, data);
+ }
+
+ async deleteConnection(connectionId: string) {
+ return await this.mainDo.deleteConnection(connectionId, this.userId);
+ }
+
+ async findFirstConnection(): Promise {
+ return await this.mainDo.findFirstConnection(this.userId);
+ }
+
+ async findManyConnections(): Promise<(typeof connection.$inferSelect)[]> {
+ return await this.mainDo.findManyConnections(this.userId);
+ }
+
+ async findManyNotesByThreadId(threadId: string): Promise<(typeof note.$inferSelect)[]> {
+ return await this.mainDo.findManyNotesByThreadId(this.userId, threadId);
+ }
+
+ async createNote(payload: Omit) {
+ return await this.mainDo.createNote(this.userId, payload as typeof note.$inferInsert);
+ }
+
+ async updateNote(noteId: string, payload: Partial) {
+ return await this.mainDo.updateNote(this.userId, noteId, payload);
+ }
+
+ async updateManyNotes(
+ notes: { id: string; order: number; isPinned?: boolean | null }[],
+ ): Promise {
+ return await this.mainDo.updateManyNotes(this.userId, notes);
+ }
+
+ async findManyNotesByIds(noteIds: string[]): Promise<(typeof note.$inferSelect)[]> {
+ return await this.mainDo.findManyNotesByIds(this.userId, noteIds);
+ }
+
+ async deleteNote(noteId: string) {
+ return await this.mainDo.deleteNote(this.userId, noteId);
+ }
+
+ async findNoteById(noteId: string): Promise {
+ return await this.mainDo.findNoteById(this.userId, noteId);
+ }
+
+ async findHighestNoteOrder(): Promise<{ order: number } | undefined> {
+ return await this.mainDo.findHighestNoteOrder(this.userId);
+ }
+
+ async deleteUser() {
+ return await this.mainDo.deleteUser(this.userId);
+ }
+
+ async findUserSettings(): Promise {
+ return await this.mainDo.findUserSettings(this.userId);
+ }
+
+ async findUserHotkeys(): Promise<(typeof userHotkeys.$inferSelect)[]> {
+ return await this.mainDo.findUserHotkeys(this.userId);
+ }
+
+ async insertUserHotkeys(shortcuts: (typeof userHotkeys.$inferInsert)[]) {
+ return await this.mainDo.insertUserHotkeys(this.userId, shortcuts);
+ }
+
+ async insertUserSettings(settings: typeof defaultUserSettings) {
+ return await this.mainDo.insertUserSettings(this.userId, settings);
+ }
+
+ async updateUserSettings(settings: typeof defaultUserSettings) {
+ return await this.mainDo.updateUserSettings(this.userId, settings);
+ }
+
+ async createConnection(
+ providerId: EProviders,
+ email: string,
+ updatingInfo: {
+ expiresAt: Date;
+ scope: string;
+ },
+ ): Promise<{ id: string }[]> {
+ return await this.mainDo.createConnection(providerId, email, this.userId, updatingInfo);
+ }
+
+ async findConnectionById(
+ connectionId: string,
+ ): Promise {
+ return await this.mainDo.findConnectionById(connectionId);
+ }
+
+ async syncUserMatrix(connectionId: string, emailStyleMatrix: EmailMatrix) {
+ return await this.mainDo.syncUserMatrix(connectionId, emailStyleMatrix);
+ }
+
+ async findWritingStyleMatrix(
+ connectionId: string,
+ ): Promise {
+ return await this.mainDo.findWritingStyleMatrix(connectionId);
+ }
+
+ async deleteActiveConnection(connectionId: string) {
+ return await this.mainDo.deleteActiveConnection(this.userId, connectionId);
+ }
+
+ async updateConnection(
+ connectionId: string,
+ updatingInfo: Partial,
+ ) {
+ return await this.mainDo.updateConnection(connectionId, updatingInfo);
+ }
+}
+
+class ZeroDB extends DurableObject {
+ db: DB = createDb(env.HYPERDRIVE.connectionString).db;
+
+ async setMetaData(userId: string) {
+ return new DbRpcDO(this, userId);
+ }
async findUser(userId: string): Promise {
return await this.db.query.user.findFirst({
@@ -367,19 +504,32 @@ export default class extends WorkerEntrypoint {
if (userId) {
const db = getZeroDB(userId);
- c.set('sessionUser', await db.findUser(userId));
+ c.set('sessionUser', await db.findUser());
+ (await db)[Symbol.dispose]?.();
}
}
}
const autumn = new Autumn({ secretKey: env.AUTUMN_SECRET_KEY });
c.set('autumn', autumn);
+
await next();
+
+ if (c.var.sessionUser?.id) {
+ const db = getZeroDB(c.var.sessionUser.id);
+ (await db)[Symbol.dispose]?.();
+ }
+
+ c.set('sessionUser', undefined);
+ c.set('autumn', undefined as any);
+ c.set('auth', undefined as any);
})
.route('/ai', aiRouter)
.route('/autumn', autumnApi)
.route('/public', publicRouter)
- .on(['GET', 'POST'], '/auth/*', (c) => c.var.auth.handler(c.req.raw))
+ .on(['GET', 'POST', 'OPTIONS'], '/auth/*', (c) => {
+ return c.var.auth.handler(c.req.raw);
+ })
.use(
trpcServer({
endpoint: '/api/trpc',
@@ -421,6 +571,10 @@ export default class extends WorkerEntrypoint {
exposeHeaders: ['X-Zero-Redirect'],
}),
)
+ .get('.well-known/oauth-authorization-server', async (c) => {
+ const auth = createAuth();
+ return oAuthDiscoveryMetadata(auth)(c.req.raw);
+ })
.mount(
'/sse',
async (request, env, ctx) => {
@@ -428,8 +582,13 @@ export default class extends WorkerEntrypoint {
if (!authBearer) {
return new Response('Unauthorized', { status: 401 });
}
+ const auth = createAuth();
+ const session = await auth.api.getMcpSession({ headers: request.headers });
+ if (!session) {
+ return new Response('Unauthorized', { status: 401 });
+ }
ctx.props = {
- cookie: authBearer,
+ userId: session?.userId,
};
return ZeroMCP.serveSSE('/sse', { binding: 'ZERO_MCP' }).fetch(request, env, ctx);
},
@@ -442,8 +601,10 @@ export default class extends WorkerEntrypoint {
if (!authBearer) {
return new Response('Unauthorized', { status: 401 });
}
+ const auth = createAuth();
+ const session = await auth.api.getMcpSession({ headers: request.headers });
ctx.props = {
- cookie: authBearer,
+ userId: session?.userId,
};
return ZeroMCP.serve('/mcp', { binding: 'ZERO_MCP' }).fetch(request, env, ctx);
},
@@ -565,31 +726,6 @@ export default class extends WorkerEntrypoint {
`Processed ${allAccounts.keys.length} accounts, found ${expiredSubscriptions.length} expired subscriptions`,
);
}
-
- public async notifyUser({
- connectionId,
- threadIds,
- type,
- }: {
- connectionId: string;
- threadIds: string[];
- type: 'refresh' | 'list';
- }) {
- console.log(`Notifying user ${connectionId} for threads ${threadIds} with type ${type}`);
- const durableObject = env.DURABLE_MAILBOX.idFromName(`${connectionId}`);
- if (env.DURABLE_MAILBOX.get(durableObject)) {
- const stub = env.DURABLE_MAILBOX.get(durableObject);
- if (stub) {
- console.log(`Broadcasting message for thread ${threadIds} with type ${type}`);
- await stub.broadcast(JSON.stringify({ threadIds, type }));
- console.log(`Successfully broadcasted message for thread ${threadIds}`);
- } else {
- console.log(`No stub found for connection ${connectionId}`);
- }
- } else {
- console.log(`No durable object found for connection ${connectionId}`);
- }
- }
}
export { DurableMailbox, ZeroAgent, ZeroMCP, MainWorkflow, ZeroWorkflow, ThreadWorkflow, ZeroDB };
diff --git a/apps/server/src/pipelines.ts b/apps/server/src/pipelines.ts
index 0b5debc383..ccdc999ce5 100644
--- a/apps/server/src/pipelines.ts
+++ b/apps/server/src/pipelines.ts
@@ -1,3 +1,16 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import {
ReSummarizeThread,
SummarizeMessage,
@@ -144,7 +157,7 @@ export class ZeroWorkflow extends WorkflowEntrypoint {
await env.gmail_processing_threads.put(historyProcessingKey, 'true');
log('[ZERO_WORKFLOW] Set processing flag for history:', historyProcessingKey);
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const foundConnection = await step.do(`[ZERO] Find Connection ${connectionId}`, async () => {
log('[ZERO_WORKFLOW] Finding connection:', connectionId);
const [foundConnection] = await db
@@ -325,6 +338,7 @@ export class ZeroWorkflow extends WorkflowEntrypoint {
});
}
}
+ this.ctx.waitUntil(conn.end());
} catch (error) {
const historyProcessingKey = `history_${event.payload.connectionId}__${event.payload.historyId}`;
try {
@@ -363,7 +377,7 @@ export class ThreadWorkflow extends WorkflowEntrypoint {
const { connectionId, threadId, providerId } = event.payload;
if (providerId === EProviders.google) {
log('[THREAD_WORKFLOW] Processing Google provider workflow');
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const foundConnection = await step.do(
`[ZERO] Find Connection ${connectionId}`,
async () => {
@@ -372,6 +386,7 @@ export class ThreadWorkflow extends WorkflowEntrypoint {
.select()
.from(connection)
.where(eq(connection.id, connectionId.toString()));
+ this.ctx.waitUntil(conn.end());
if (!foundConnection) throw new Error('Connection not found');
if (!foundConnection.accessToken || !foundConnection.refreshToken)
throw new Error('Connection is not authorized');
diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts
index 1cc5b6abba..fd07d48d71 100644
--- a/apps/server/src/routes/agent/tools.ts
+++ b/apps/server/src/routes/agent/tools.ts
@@ -65,7 +65,7 @@ const askZeroMailbox = (connectionId: string) =>
};
}
return {
- response: threadResults.matches.map((e) => e.metadata?.['content'] ?? 'no content'),
+ response: threadResults.matches.map((e) => e.metadata?.['summary'] ?? 'no content'),
success: true,
};
},
@@ -96,7 +96,7 @@ const askZeroThread = (connectionId: string) =>
const topThread = threadResults.matches[0];
if (!topThread) return { response: "I don't know, no threads found", success: false };
return {
- response: topThread.metadata?.['content'] ?? 'no content',
+ response: topThread.metadata?.['summary'] ?? 'no content',
success: true,
};
},
diff --git a/apps/server/src/routes/ai.ts b/apps/server/src/routes/ai.ts
index 3edb6f1b0b..7234dd2a45 100644
--- a/apps/server/src/routes/ai.ts
+++ b/apps/server/src/routes/ai.ts
@@ -22,7 +22,7 @@ aiRouter.post('/do/:action', async (c) => {
if (env.VOICE_SECRET !== c.req.header('X-Voice-Secret'))
return c.json({ success: false, error: 'Unauthorized' }, 401);
if (!c.req.header('X-Caller')) return c.json({ success: false, error: 'Unauthorized' }, 401);
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const user = await db.query.user.findFirst({
where: (user, { eq, and }) =>
and(eq(user.phoneNumber, c.req.header('X-Caller')!), eq(user.phoneNumberVerified, true)),
@@ -33,6 +33,7 @@ aiRouter.post('/do/:action', async (c) => {
where: (connection, { eq, or }) =>
or(eq(connection.id, user.defaultConnectionId!), eq(connection.userId, user.id)),
});
+ await conn.end();
if (!connection) return c.json({ success: false, error: 'Unauthorized' }, 401);
try {
@@ -114,7 +115,7 @@ aiRouter.post('/call', async (c) => {
}
console.log('[DEBUG] Connecting to database');
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
console.log('[DEBUG] Finding user by phone number:', c.req.header('X-Caller'));
const user = await db.query.user.findFirst({
@@ -133,6 +134,8 @@ aiRouter.post('/call', async (c) => {
or(eq(connection.id, user.defaultConnectionId!), eq(connection.userId, user.id)),
});
+ await conn.end();
+
if (!connection) {
console.log('[DEBUG] No connection found for user');
return c.json({ success: false, error: 'Unauthorized' }, 401);
diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts
index 75e07755a7..9af6a11564 100644
--- a/apps/server/src/routes/chat.ts
+++ b/apps/server/src/routes/chat.ts
@@ -8,26 +8,30 @@ import {
appendResponseMessages,
} from 'ai';
import {
- AiChatPrompt,
getCurrentDateContext,
GmailSearchAssistantSystemPrompt,
+ AiChatPrompt,
} from '../lib/prompts';
import { type Connection, type ConnectionContext, type WSMessage } from 'agents';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createSimpleAuth, type SimpleAuth } from '../lib/auth';
+import { EPrompts, type IOutgoingMessage } from '../types';
import { connectionToDriver } from '../lib/server-utils';
import type { MailManager } from '../lib/driver/types';
+import type { CreateDraftData } from '../lib/schemas';
import { FOLDERS, parseHeaders } from '../lib/utils';
+import { env, RpcTarget } from 'cloudflare:workers';
import { AIChatAgent } from 'agents/ai-chat-agent';
import { tools as authTools } from './agent/tools';
import { processToolCalls } from './agent/utils';
import type { Message as ChatMessage } from 'ai';
+import { getPromptName } from '../pipelines';
import { connection } from '../db/schema';
-import { env } from 'cloudflare:workers';
+import { getPrompt } from '../lib/brain';
import { openai } from '@ai-sdk/openai';
+import { and, eq } from 'drizzle-orm';
import { McpAgent } from 'agents/mcp';
import { groq } from '@ai-sdk/groq';
-import { eq } from 'drizzle-orm';
import { createDb } from '../db';
import { z } from 'zod';
@@ -105,6 +109,149 @@ export type OutgoingMessage =
};
};
+export class AgentRpcDO extends RpcTarget {
+ constructor(
+ private mainDo: ZeroAgent,
+ private connectionId: string,
+ ) {
+ super();
+ }
+
+ async getUserLabels() {
+ return await this.mainDo.getUserLabels();
+ }
+
+ async getLabel(id: string) {
+ return await this.mainDo.getLabel(id);
+ }
+
+ async createLabel(label: {
+ name: string;
+ color?: { backgroundColor: string; textColor: string };
+ }) {
+ return await this.mainDo.createLabel(label);
+ }
+
+ async updateLabel(
+ id: string,
+ label: { name: string; color?: { backgroundColor: string; textColor: string } },
+ ) {
+ return await this.mainDo.updateLabel(id, label);
+ }
+
+ async deleteLabel(id: string) {
+ return await this.mainDo.deleteLabel(id);
+ }
+
+ async bulkDelete(threadIds: string[]) {
+ return await this.mainDo.bulkDelete(threadIds);
+ }
+
+ async bulkArchive(threadIds: string[]) {
+ return await this.mainDo.bulkArchive(threadIds);
+ }
+
+ async buildGmailSearchQuery(query: string) {
+ return await this.mainDo.buildGmailSearchQuery(query);
+ }
+
+ async listThreads(params: {
+ folder: string;
+ query?: string;
+ maxResults?: number;
+ labelIds?: string[];
+ pageToken?: string;
+ }) {
+ return await this.mainDo.listThreads(params);
+ }
+
+ async getThread(threadId: string) {
+ return await this.mainDo.getThread(threadId);
+ }
+
+ async markThreadsRead(threadIds: string[]) {
+ return await this.mainDo.markThreadsRead(threadIds);
+ }
+
+ async markThreadsUnread(threadIds: string[]) {
+ return await this.mainDo.markThreadsUnread(threadIds);
+ }
+
+ async modifyLabels(threadIds: string[], addLabelIds: string[], removeLabelIds: string[]) {
+ return await this.mainDo.modifyLabels(threadIds, addLabelIds, removeLabelIds);
+ }
+
+ async createDraft(draftData: CreateDraftData) {
+ return await this.mainDo.createDraft(draftData);
+ }
+
+ async getDraft(id: string) {
+ return await this.mainDo.getDraft(id);
+ }
+
+ async listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }) {
+ return await this.mainDo.listDrafts(params);
+ }
+
+ async count() {
+ return await this.mainDo.count();
+ }
+
+ async list(params: {
+ folder: string;
+ query?: string;
+ maxResults?: number;
+ labelIds?: string[];
+ pageToken?: string;
+ }) {
+ return await this.mainDo.list(params);
+ }
+
+ async markAsRead(threadIds: string[]) {
+ return await this.mainDo.markAsRead(threadIds);
+ }
+
+ async markAsUnread(threadIds: string[]) {
+ return await this.mainDo.markAsUnread(threadIds);
+ }
+
+ async normalizeIds(ids: string[]) {
+ return await this.mainDo.normalizeIds(ids);
+ }
+
+ async get(id: string) {
+ return await this.mainDo.get(id);
+ }
+
+ async sendDraft(id: string, data: IOutgoingMessage) {
+ return await this.mainDo.sendDraft(id, data);
+ }
+
+ async create(data: IOutgoingMessage) {
+ return await this.mainDo.create(data);
+ }
+
+ async delete(id: string) {
+ return await this.mainDo.delete(id);
+ }
+
+ async deleteAllSpam() {
+ return await this.mainDo.deleteAllSpam();
+ }
+
+ async getEmailAliases() {
+ return await this.mainDo.getEmailAliases();
+ }
+
+ async setupAuth(connectionId: string) {
+ return await this.mainDo.setupAuth(connectionId);
+ }
+
+ async broadcast(message: string) {
+ return this.mainDo.broadcast(message);
+ }
+}
+
export class ZeroAgent extends AIChatAgent {
private chatMessageAbortControllers: Map = new Map();
driver: MailManager | null = null;
@@ -112,6 +259,10 @@ export class ZeroAgent extends AIChatAgent {
super(ctx, env);
}
+ async setMetaData(connectionId: string) {
+ return new AgentRpcDO(this, connectionId);
+ }
+
private getDataStreamResponse(
onFinish: StreamTextOnFinishCallback<{}>,
options?: {
@@ -120,10 +271,10 @@ export class ZeroAgent extends AIChatAgent {
) {
const dataStreamResponse = createDataStreamResponse({
execute: async (dataStream) => {
- const connectionId = (await this.ctx.storage.get('connectionId')) as string;
+ const connectionId = this.name;
if (!connectionId || !this.driver) {
console.log('Unauthorized no driver or connectionId [1]', connectionId, this.driver);
- await this.setupAuth();
+ await this.setupAuth(connectionId);
if (!connectionId || !this.driver) {
console.log('Unauthorized no driver or connectionId', connectionId, this.driver);
throw new Error('Unauthorized no driver or connectionId [2]');
@@ -144,7 +295,10 @@ export class ZeroAgent extends AIChatAgent {
messages: processedMessages,
tools,
onFinish,
- system: AiChatPrompt('', '', ''),
+ system: await getPrompt(
+ getPromptName(connectionId, EPrompts.Chat),
+ AiChatPrompt('', '', ''),
+ ),
});
result.mergeIntoDataStream(dataStream);
@@ -154,16 +308,14 @@ export class ZeroAgent extends AIChatAgent {
return dataStreamResponse;
}
- private async setupAuth() {
- if (this.name) {
- const db = createDb(env.HYPERDRIVE.connectionString);
+ public async setupAuth(connectionId: string) {
+ if (!this.driver) {
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const _connection = await db.query.connection.findFirst({
- where: eq(connection.id, this.name),
+ where: eq(connection.id, connectionId),
});
- if (_connection) {
- await this.ctx.storage.put('connectionId', _connection.id);
- this.driver = connectionToDriver(_connection);
- }
+ if (_connection) this.driver = connectionToDriver(_connection);
+ this.ctx.waitUntil(conn.end());
}
}
@@ -331,7 +483,7 @@ export class ZeroAgent extends AIChatAgent {
}
async onConnect() {
- await this.setupAuth();
+ await this.setupAuth(this.name);
}
private destroyAbortControllers() {
@@ -349,35 +501,316 @@ export class ZeroAgent extends AIChatAgent {
) {
return this.getDataStreamResponse(onFinish, options);
}
+
+ async listThreads(params: {
+ folder: string;
+ query?: string;
+ maxResults?: number;
+ labelIds?: string[];
+ pageToken?: string;
+ }) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.list(params);
+ }
+
+ async getThread(threadId: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.get(threadId);
+ }
+
+ async markThreadsRead(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.modifyLabels(threadIds, {
+ addLabels: [],
+ removeLabels: ['UNREAD'],
+ });
+ }
+
+ async markThreadsUnread(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.modifyLabels(threadIds, {
+ addLabels: ['UNREAD'],
+ removeLabels: [],
+ });
+ }
+
+ async modifyLabels(threadIds: string[], addLabelIds: string[], removeLabelIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.modifyLabels(threadIds, {
+ addLabels: addLabelIds,
+ removeLabels: removeLabelIds,
+ });
+ }
+
+ async getUserLabels() {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return (await this.driver.getUserLabels()).filter((label) => label.type === 'user');
+ }
+
+ async getLabel(id: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.getLabel(id);
+ }
+
+ async createLabel(params: {
+ name: string;
+ color?: {
+ backgroundColor: string;
+ textColor: string;
+ };
+ }) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.createLabel(params);
+ }
+
+ async bulkDelete(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.modifyLabels(threadIds, {
+ addLabels: ['TRASH'],
+ removeLabels: ['INBOX'],
+ });
+ }
+
+ async bulkArchive(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.modifyLabels(threadIds, {
+ addLabels: [],
+ removeLabels: ['INBOX'],
+ });
+ }
+
+ async buildGmailSearchQuery(query: string) {
+ const result = await generateText({
+ model: openai('gpt-4o'),
+ system: GmailSearchAssistantSystemPrompt(),
+ prompt: query,
+ });
+ return result.text;
+ }
+
+ async updateLabel(
+ id: string,
+ label: { name: string; color?: { backgroundColor: string; textColor: string } },
+ ) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.updateLabel(id, label);
+ }
+
+ async deleteLabel(id: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.deleteLabel(id);
+ }
+
+ async createDraft(draftData: CreateDraftData) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.createDraft(draftData);
+ }
+
+ async getDraft(id: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.getDraft(id);
+ }
+
+ async listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.listDrafts(params);
+ }
+
+ // Additional mail operations
+ async count() {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.count();
+ }
+
+ async list(params: {
+ folder: string;
+ query?: string;
+ maxResults?: number;
+ labelIds?: string[];
+ pageToken?: string;
+ }) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.list(params);
+ }
+
+ async markAsRead(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.markAsRead(threadIds);
+ }
+
+ async markAsUnread(threadIds: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.markAsUnread(threadIds);
+ }
+
+ async normalizeIds(ids: string[]) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return this.driver.normalizeIds(ids);
+ }
+
+ async get(id: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.get(id);
+ }
+
+ async sendDraft(id: string, data: IOutgoingMessage) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.sendDraft(id, data);
+ }
+
+ async create(data: IOutgoingMessage) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.create(data);
+ }
+
+ async delete(id: string) {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.delete(id);
+ }
+
+ async deleteAllSpam() {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.deleteAllSpam();
+ }
+
+ async getEmailAliases() {
+ if (!this.driver) {
+ throw new Error('No driver available');
+ }
+ return await this.driver.getEmailAliases();
+ }
}
-export class ZeroMCP extends McpAgent {
- auth: SimpleAuth;
+export class ZeroMCP extends McpAgent {
server = new McpServer({
name: 'zero-mcp',
version: '1.0.0',
description: 'Zero MCP',
});
+ activeConnectionId: string | undefined;
+
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
- this.auth = createSimpleAuth();
}
async init(): Promise {
- const session = await this.auth.api.getSession({ headers: parseHeaders(this.props.cookie) });
- if (!session) {
- throw new Error('Unauthorized');
- }
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const _connection = await db.query.connection.findFirst({
- where: eq(connection.email, session.user.email),
+ where: eq(connection.userId, this.props.userId),
});
if (!_connection) {
throw new Error('Unauthorized');
}
+ this.activeConnectionId = _connection.id;
const driver = connectionToDriver(_connection);
+ this.server.tool('getConnections', async () => {
+ const connections = await db.query.connection.findMany({
+ where: eq(connection.userId, this.props.userId),
+ });
+ return {
+ content: connections.map((c) => ({
+ type: 'text',
+ text: `Email: ${c.email} | Provider: ${c.providerId}`,
+ })),
+ };
+ });
+
+ this.server.tool('getActiveConnection', async () => {
+ if (!this.activeConnectionId) {
+ throw new Error('No active connection');
+ }
+ const _connection = await db.query.connection.findFirst({
+ where: eq(connection.id, this.activeConnectionId),
+ });
+ if (!_connection) {
+ throw new Error('Connection not found');
+ }
+ return {
+ content: [
+ {
+ type: 'text' as const,
+ text: `Email: ${_connection.email} | Provider: ${_connection.providerId}`,
+ },
+ ],
+ };
+ });
+
+ this.server.tool(
+ 'setActiveConnection',
+ {
+ email: z.string(),
+ },
+ async (s) => {
+ const _connection = await db.query.connection.findFirst({
+ where: and(eq(connection.userId, this.props.userId), eq(connection.email, s.email)),
+ });
+ if (!_connection) {
+ throw new Error('Connection not found');
+ }
+ this.activeConnectionId = _connection.id;
+ return {
+ content: [
+ {
+ type: 'text' as const,
+ text: `Active connection set to ${_connection.email}`,
+ },
+ ],
+ };
+ },
+ );
+
this.server.tool(
'buildGmailSearchQuery',
{
@@ -423,7 +856,11 @@ export class ZeroMCP extends McpAgent {
return [
{
type: 'text' as const,
- text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Received: ${loadedThread.latest?.receivedOn}`,
+ text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Latest Message Received: ${loadedThread.latest?.receivedOn}`,
+ },
+ {
+ type: 'text' as const,
+ text: `Latest Message Sender: ${loadedThread.latest?.sender}`,
},
];
}),
@@ -448,28 +885,54 @@ export class ZeroMCP extends McpAgent {
},
async (s) => {
const thread = await driver.get(s.threadId);
+ const initialResponse = [
+ {
+ type: 'text' as const,
+ text: `Subject: ${thread.latest?.subject}`,
+ },
+ {
+ type: 'text' as const,
+ text: `Latest Message Received: ${thread.latest?.receivedOn}`,
+ },
+ {
+ type: 'text' as const,
+ text: `Latest Message Sender: ${thread.latest?.sender}`,
+ },
+ {
+ type: 'text' as const,
+ text: `Latest Message Raw Content: ${thread.latest?.decodedBody}`,
+ },
+ {
+ type: 'text' as const,
+ text: `Thread ID: ${s.threadId}`,
+ },
+ ];
const response = await env.VECTORIZE.getByIds([s.threadId]);
- if (response.length && response?.[0]?.metadata?.['content']) {
- const content = response[0].metadata['content'] as string;
+ if (response.length && response?.[0]?.metadata?.['summary']) {
+ const content = response[0].metadata['summary'] as string;
const shortResponse = await env.AI.run('@cf/facebook/bart-large-cnn', {
input_text: content,
});
return {
content: [
+ ...initialResponse,
{
type: 'text',
- text: shortResponse.summary,
+ text: `Subject: ${thread.latest?.subject}`,
+ },
+ {
+ type: 'text',
+ text: `Long Summary: ${content}`,
+ },
+ {
+ type: 'text',
+ text: `Short Summary: ${shortResponse.summary}`,
},
],
};
}
return {
- content: [
- {
- type: 'text',
- text: `Subject: ${thread.latest?.subject}`,
- },
- ],
+ content: initialResponse,
};
},
);
@@ -689,6 +1152,7 @@ export class ZeroMCP extends McpAgent {
}
},
);
+ this.ctx.waitUntil(conn.end());
}
}
diff --git a/apps/server/src/services/mcp-service/mcp.ts b/apps/server/src/services/mcp-service/mcp.ts
index 1ecd2bfb1b..71bb913509 100644
--- a/apps/server/src/services/mcp-service/mcp.ts
+++ b/apps/server/src/services/mcp-service/mcp.ts
@@ -10,7 +10,7 @@ import { generateText } from 'ai';
import { z } from 'zod';
export const getDriverFromConnectionId = async (connectionId: string) => {
- const db = createDb(env.HYPERDRIVE.connectionString);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
const activeConnection = await db.query.connection.findFirst({
where: (connection, ops) => ops.eq(connection.id, connectionId),
columns: {
@@ -22,6 +22,8 @@ export const getDriverFromConnectionId = async (connectionId: string) => {
},
});
+ await conn.end();
+
if (!activeConnection || !activeConnection.accessToken || !activeConnection.refreshToken) {
throw new Error('No connection found');
}
@@ -138,8 +140,8 @@ export class ZeroMCP extends McpAgent
};
// const response = await env.VECTORIZE.getByIds([s.threadId]);
- // if (response.length && response?.[0]?.metadata?.['content']) {
- // const content = response[0].metadata['content'] as string;
+ // if (response.length && response?.[0]?.metadata?.['summary']) {
+ // const content = response[0].metadata['summary'] as string;
// const shortResponse = await env.AI.run('@cf/facebook/bart-large-cnn', {
// input_text: content,
// });
diff --git a/apps/server/src/services/writing-style-service.ts b/apps/server/src/services/writing-style-service.ts
index 84d1556e09..3ff1e1f894 100644
--- a/apps/server/src/services/writing-style-service.ts
+++ b/apps/server/src/services/writing-style-service.ts
@@ -8,6 +8,7 @@ import { google } from '@ai-sdk/google';
import { jsonrepair } from 'jsonrepair';
import { generateObject } from 'ai';
import { eq } from 'drizzle-orm';
+import { createDb } from '../db';
import pRetry from 'p-retry';
import { z } from 'zod';
@@ -164,9 +165,13 @@ export const getWritingStyleMatrixForConnectionId = async ({
connectionId: string;
backupContent?: string;
}) => {
- const db = getZeroDB('global-db');
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
- const matrix = await db.findWritingStyleMatrix(connectionId);
+ const matrix = await db.query.writingStyleMatrix.findFirst({
+ where: eq(writingStyleMatrix.connectionId, connectionId),
+ });
+
+ await conn.end();
if (!matrix && backupContent) {
if (!backupContent.trim()) {
@@ -190,8 +195,44 @@ export const updateWritingStyleMatrix = async (connectionId: string, emailBody:
await pRetry(
async () => {
- const db = getZeroDB('global-db');
- await db.syncUserMatrix(connectionId, emailStyleMatrix);
+ const { db, conn } = createDb(env.HYPERDRIVE.connectionString);
+ await db.transaction(async (tx) => {
+ const [existingMatrix] = await tx
+ .select({
+ numMessages: writingStyleMatrix.numMessages,
+ style: writingStyleMatrix.style,
+ })
+ .from(writingStyleMatrix)
+ .where(eq(writingStyleMatrix.connectionId, connectionId));
+
+ if (existingMatrix) {
+ const newStyle = createUpdatedMatrixFromNewEmail(
+ existingMatrix.numMessages,
+ existingMatrix.style as WritingStyleMatrix,
+ emailStyleMatrix,
+ );
+
+ await tx
+ .update(writingStyleMatrix)
+ .set({
+ numMessages: existingMatrix.numMessages + 1,
+ style: newStyle,
+ })
+ .where(eq(writingStyleMatrix.connectionId, connectionId));
+ } else {
+ const newStyle = initializeStyleMatrixFromEmail(emailStyleMatrix);
+
+ await tx
+ .insert(writingStyleMatrix)
+ .values({
+ connectionId,
+ numMessages: 1,
+ style: newStyle,
+ })
+ .onConflictDoNothing();
+ }
+ });
+ await conn.end();
},
{
retries: 1,
diff --git a/apps/server/src/trpc/routes/ai/compose.ts b/apps/server/src/trpc/routes/ai/compose.ts
index 11c7d96f40..9ab24a7cc3 100644
--- a/apps/server/src/trpc/routes/ai/compose.ts
+++ b/apps/server/src/trpc/routes/ai/compose.ts
@@ -3,6 +3,8 @@ import {
type WritingStyleMatrix,
} from '../../../services/writing-style-service';
import { StyledEmailAssistantSystemPrompt } from '../../../lib/prompts';
+import { getPrompt } from '../../../lib/brain';
+import { EPrompts } from '../../../types';
import { webSearch } from '../../../routes/agent/tools';
import { activeConnectionProcedure } from '../../trpc';
import { stripHtml } from 'string-strip-html';
@@ -33,7 +35,10 @@ export async function composeEmail(input: ComposeEmailInput) {
connectionId,
});
- const systemPrompt = StyledEmailAssistantSystemPrompt();
+ const systemPrompt = await getPrompt(
+ `${connectionId}-${EPrompts.Compose}`,
+ StyledEmailAssistantSystemPrompt()
+ );
const userPrompt = EmailAssistantPrompt({
currentSubject: emailSubject,
recipients: [...(to ?? []), ...(cc ?? [])],
diff --git a/apps/server/src/trpc/routes/brain.ts b/apps/server/src/trpc/routes/brain.ts
index 7cdedf5345..75c773b893 100644
--- a/apps/server/src/trpc/routes/brain.ts
+++ b/apps/server/src/trpc/routes/brain.ts
@@ -1,5 +1,5 @@
import { disableBrainFunction, getPrompts } from '../../lib/brain';
-import { EProviders, type ISubscribeBatch } from '../../types';
+import { EProviders, EPrompts, type ISubscribeBatch } from '../../types';
import { activeConnectionProcedure, router } from '../trpc';
import { setSubscribedState } from '../../lib/utils';
import { env } from 'cloudflare:workers';
@@ -80,6 +80,22 @@ export const brainRouter = router({
const connection = ctx.activeConnection;
return await getPrompts({ connectionId: connection.id });
}),
+ updatePrompt: activeConnectionProcedure
+ .input(
+ z.object({
+ promptType: z.nativeEnum(EPrompts),
+ content: z.string(),
+ }),
+ )
+ .mutation(async ({ ctx, input }) => {
+ const connection = ctx.activeConnection;
+
+ const promptName = `${connection.id}-${input.promptType}`;
+
+ await env.prompts_storage.put(promptName, input.content);
+
+ return { success: true };
+ }),
updateLabels: activeConnectionProcedure
.input(
z.object({
diff --git a/apps/server/src/trpc/routes/connections.ts b/apps/server/src/trpc/routes/connections.ts
index b95bcc003d..1ae84d9995 100644
--- a/apps/server/src/trpc/routes/connections.ts
+++ b/apps/server/src/trpc/routes/connections.ts
@@ -18,7 +18,7 @@ export const connectionsRouter = router({
.query(async ({ ctx }) => {
const { sessionUser } = ctx;
const db = getZeroDB(sessionUser.id);
- const connections = await db.findManyConnections(sessionUser.id);
+ const connections = await db.findManyConnections();
const disconnectedIds = connections
.filter((c) => !c.accessToken || !c.refreshToken)
@@ -44,9 +44,9 @@ export const connectionsRouter = router({
const { connectionId } = input;
const user = ctx.sessionUser;
const db = getZeroDB(user.id);
- const foundConnection = await db.findUserConnection(user.id, connectionId);
+ const foundConnection = await db.findUserConnection(connectionId);
if (!foundConnection) throw new TRPCError({ code: 'NOT_FOUND' });
- await db.updateUser(user.id, { defaultConnectionId: connectionId });
+ await db.updateUser({ defaultConnectionId: connectionId });
}),
delete: privateProcedure
.input(z.object({ connectionId: z.string() }))
@@ -54,11 +54,10 @@ export const connectionsRouter = router({
const { connectionId } = input;
const user = ctx.sessionUser;
const db = getZeroDB(user.id);
- await db.deleteConnection(connectionId, user.id);
+ await db.deleteConnection(connectionId);
const activeConnection = await getActiveConnection();
- if (connectionId === activeConnection.id)
- await db.updateUser(user.id, { defaultConnectionId: null });
+ if (connectionId === activeConnection.id) await db.updateUser({ defaultConnectionId: null });
}),
getDefault: publicProcedure.query(async ({ ctx }) => {
if (!ctx.sessionUser) return null;
diff --git a/apps/server/src/trpc/routes/drafts.ts b/apps/server/src/trpc/routes/drafts.ts
index 88b576c7ca..d3c1775a4f 100644
--- a/apps/server/src/trpc/routes/drafts.ts
+++ b/apps/server/src/trpc/routes/drafts.ts
@@ -1,16 +1,20 @@
+import type { MailManager } from '../../lib/driver/types';
import { activeDriverProcedure, router } from '../trpc';
+import { getZeroAgent } from '../../lib/server-utils';
import { createDraftData } from '../../lib/schemas';
import { z } from 'zod';
export const draftsRouter = router({
create: activeDriverProcedure.input(createDraftData).mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.createDraft(input);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.createDraft(input);
}),
get: activeDriverProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const { id } = input;
- return driver.getDraft(id);
+ return agent.getDraft(id) as Awaited>;
}),
list: activeDriverProcedure
.input(
@@ -21,8 +25,11 @@ export const draftsRouter = router({
}),
)
.query(async ({ input, ctx }) => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const { q, max, pageToken } = input;
- return driver.listDrafts({ q, maxResults: max, pageToken });
+ return agent.listDrafts({ q, maxResults: max, pageToken }) as Awaited<
+ ReturnType
+ >;
}),
});
diff --git a/apps/server/src/trpc/routes/label.ts b/apps/server/src/trpc/routes/label.ts
index 72b6c10208..281bf0d7cb 100644
--- a/apps/server/src/trpc/routes/label.ts
+++ b/apps/server/src/trpc/routes/label.ts
@@ -1,4 +1,5 @@
import { activeDriverProcedure, createRateLimiterMiddleware, router } from '../trpc';
+import { getZeroAgent } from '../../lib/server-utils';
import { Ratelimit } from '@upstash/ratelimit';
import { z } from 'zod';
@@ -10,9 +11,25 @@ export const labelsRouter = router({
limiter: Ratelimit.slidingWindow(60, '1m'),
}),
)
+ .output(
+ z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ color: z
+ .object({
+ backgroundColor: z.string(),
+ textColor: z.string(),
+ })
+ .optional(),
+ type: z.string(),
+ }),
+ ),
+ )
.query(async ({ ctx }) => {
- const { driver } = ctx;
- return (await driver.getUserLabels()).filter((label) => label.type === 'user');
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return await agent.getUserLabels();
}),
create: activeDriverProcedure
.use(
@@ -36,12 +53,13 @@ export const labelsRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const label = {
...input,
type: 'user',
};
- return await driver.createLabel(label);
+ return await agent.createLabel(label);
}),
update: activeDriverProcedure
.use(
@@ -64,9 +82,10 @@ export const labelsRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const { id, ...label } = input;
- return await driver.updateLabel(id, label);
+ return await agent.updateLabel(id, label);
}),
delete: activeDriverProcedure
.use(
@@ -77,7 +96,8 @@ export const labelsRouter = router({
)
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
- const { driver } = ctx;
- return await driver.deleteLabel(input.id);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return await agent.deleteLabel(input.id);
}),
});
diff --git a/apps/server/src/trpc/routes/mail.ts b/apps/server/src/trpc/routes/mail.ts
index 86c638f0c4..913766b108 100644
--- a/apps/server/src/trpc/routes/mail.ts
+++ b/apps/server/src/trpc/routes/mail.ts
@@ -3,7 +3,8 @@ import { updateWritingStyleMatrix } from '../../services/writing-style-service';
import { deserializeFiles, serializedFileSchema } from '../../lib/schemas';
import { defaultPageSize, FOLDERS, LABELS } from '../../lib/utils';
import type { DeleteAllSpamResponse } from '../../types';
-import { Ratelimit } from '@upstash/ratelimit';
+import { getZeroAgent } from '../../lib/server-utils';
+import { env } from 'cloudflare:workers';
import { z } from 'zod';
const senderSchema = z.object({
@@ -19,13 +20,24 @@ export const mailRouter = router({
}),
)
.query(async ({ input, ctx }) => {
- const { driver } = ctx;
- return await driver.get(input.id);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return await agent.getThread(input.id);
+ }),
+ count: activeDriverProcedure
+ .output(
+ z.array(
+ z.object({
+ count: z.number().optional(),
+ label: z.string().optional(),
+ }),
+ ),
+ )
+ .query(async ({ ctx }) => {
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return await agent.count();
}),
- count: activeDriverProcedure.query(async ({ ctx }) => {
- const { driver } = ctx;
- return await driver.count();
- }),
listThreads: activeDriverProcedure
.input(
z.object({
@@ -35,22 +47,20 @@ export const mailRouter = router({
cursor: z.string().optional().default(''),
}),
)
- // .use(
- // createRateLimiterMiddleware({
- // generatePrefix: ({ sessionUser }, input) =>
- // `ratelimit:list-threads-${input.folder}-${sessionUser?.id}`,
- // limiter: Ratelimit.slidingWindow(60, '1m'),
- // }),
- // )
.query(async ({ ctx, input }) => {
const { folder, max, cursor, q } = input;
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
if (folder === FOLDERS.DRAFT) {
- const drafts = await driver.listDrafts({ q, maxResults: max, pageToken: cursor });
+ const drafts = await agent.listDrafts({
+ q,
+ maxResults: max,
+ pageToken: cursor,
+ });
return drafts;
}
- const threadsResponse = await driver.list({
+ const threadsResponse = await agent.list({
folder,
query: q,
maxResults: max,
@@ -65,8 +75,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.markAsRead(input.ids);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.markAsRead(input.ids);
}),
markAsUnread: activeDriverProcedure
.input(
@@ -75,8 +86,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.markAsUnread(input.ids);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.markAsUnread(input.ids);
}),
markAsImportant: activeDriverProcedure
.input(
@@ -85,8 +97,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: ['IMPORTANT'], removeLabels: [] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, ['IMPORTANT'], []);
}),
modifyLabels: activeDriverProcedure
.input(
@@ -97,20 +110,19 @@ export const mailRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const { threadId, addLabels, removeLabels } = input;
console.log(`Server: updateThreadLabels called for thread ${threadId}`);
console.log(`Adding labels: ${addLabels.join(', ')}`);
console.log(`Removing labels: ${removeLabels.join(', ')}`);
- const { threadIds } = driver.normalizeIds(threadId);
+ const result = await agent.normalizeIds(threadId);
+ const { threadIds } = result;
if (threadIds.length) {
- await driver.modifyLabels(threadIds, {
- addLabels,
- removeLabels,
- });
+ await agent.modifyLabels(threadIds, addLabels, removeLabels);
console.log('Server: Successfully updated thread labels');
return { success: true };
}
@@ -126,14 +138,16 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- const { threadIds } = driver.normalizeIds(input.ids);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ const { threadIds } = await agent.normalizeIds(input.ids);
if (!threadIds.length) {
return { success: false, error: 'No thread IDs provided' };
}
- const threadResults = await Promise.allSettled(threadIds.map((id) => driver.get(id)));
+ const threadResults: PromiseSettledResult<{ messages: { tags: { name: string }[] }[] }>[] =
+ await Promise.allSettled(threadIds.map((id) => agent.get(id)));
let anyStarred = false;
let processedThreads = 0;
@@ -153,10 +167,11 @@ export const mailRouter = router({
const shouldStar = processedThreads > 0 && !anyStarred;
- await driver.modifyLabels(threadIds, {
- addLabels: shouldStar ? ['STARRED'] : [],
- removeLabels: shouldStar ? [] : ['STARRED'],
- });
+ await agent.modifyLabels(
+ threadIds,
+ shouldStar ? ['STARRED'] : [],
+ shouldStar ? [] : ['STARRED'],
+ );
return { success: true };
}),
@@ -167,14 +182,16 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- const { threadIds } = driver.normalizeIds(input.ids);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ const { threadIds } = await agent.normalizeIds(input.ids);
if (!threadIds.length) {
return { success: false, error: 'No thread IDs provided' };
}
- const threadResults = await Promise.allSettled(threadIds.map((id) => driver.get(id)));
+ const threadResults: PromiseSettledResult<{ messages: { tags: { name: string }[] }[] }>[] =
+ await Promise.allSettled(threadIds.map((id) => agent.get(id)));
let anyImportant = false;
let processedThreads = 0;
@@ -194,10 +211,11 @@ export const mailRouter = router({
const shouldMarkImportant = processedThreads > 0 && !anyImportant;
- await driver.modifyLabels(threadIds, {
- addLabels: shouldMarkImportant ? ['IMPORTANT'] : [],
- removeLabels: shouldMarkImportant ? [] : ['IMPORTANT'],
- });
+ await agent.modifyLabels(
+ threadIds,
+ shouldMarkImportant ? ['IMPORTANT'] : [],
+ shouldMarkImportant ? [] : ['IMPORTANT'],
+ );
return { success: true };
}),
@@ -208,8 +226,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: ['STARRED'], removeLabels: [] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, ['STARRED'], []);
}),
bulkMarkImportant: activeDriverProcedure
.input(
@@ -218,8 +237,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: ['IMPORTANT'], removeLabels: [] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, ['IMPORTANT'], []);
}),
bulkUnstar: activeDriverProcedure
.input(
@@ -228,13 +248,15 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['STARRED'] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, [], ['STARRED']);
}),
deleteAllSpam: activeDriverProcedure.mutation(async ({ ctx }): Promise => {
- const { driver } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
try {
- return await driver.deleteAllSpam();
+ return await agent.deleteAllSpam();
} catch (error) {
console.error('Error deleting spam emails:', error);
return {
@@ -252,8 +274,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['IMPORTANT'] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, [], ['IMPORTANT']);
}),
send: activeDriverProcedure
@@ -262,11 +285,7 @@ export const mailRouter = router({
to: z.array(senderSchema),
subject: z.string(),
message: z.string(),
- attachments: z
- .array(serializedFileSchema)
- .transform(deserializeFiles)
- .optional()
- .default([]),
+ attachments: z.array(serializedFileSchema).optional().default([]),
headers: z.record(z.string()).optional().default({}),
cc: z.array(senderSchema).optional(),
bcc: z.array(senderSchema).optional(),
@@ -278,7 +297,8 @@ export const mailRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
- const { driver, activeConnection } = ctx;
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
const { draftId, ...mail } = input;
const afterTask = async () => {
@@ -292,9 +312,9 @@ export const mailRouter = router({
};
if (draftId) {
- await driver.sendDraft(draftId, mail);
+ await agent.sendDraft(draftId, mail);
} else {
- await driver.create(input);
+ await agent.create(input);
}
ctx.c.executionCtx.waitUntil(afterTask());
@@ -307,8 +327,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.delete(input.id);
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.delete(input.id);
}),
bulkDelete: activeDriverProcedure
.input(
@@ -317,8 +338,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: ['TRASH'], removeLabels: [] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, ['TRASH'], []);
}),
bulkArchive: activeDriverProcedure
.input(
@@ -327,8 +349,9 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['INBOX'] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, [], ['INBOX']);
}),
bulkMute: activeDriverProcedure
.input(
@@ -337,11 +360,13 @@ export const mailRouter = router({
}),
)
.mutation(async ({ input, ctx }) => {
- const { driver } = ctx;
- return driver.modifyLabels(input.ids, { addLabels: ['MUTE'], removeLabels: [] });
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.modifyLabels(input.ids, ['MUTE'], []);
}),
getEmailAliases: activeDriverProcedure.query(async ({ ctx }) => {
- const { driver } = ctx;
- return driver.getEmailAliases();
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.getEmailAliases();
}),
});
diff --git a/apps/server/src/trpc/routes/settings.ts b/apps/server/src/trpc/routes/settings.ts
index bd51f4df4a..dde22250f6 100644
--- a/apps/server/src/trpc/routes/settings.ts
+++ b/apps/server/src/trpc/routes/settings.ts
@@ -17,14 +17,14 @@ export const settingsRouter = router({
const { sessionUser } = ctx;
const db = getZeroDB(sessionUser.id);
- const result: any = await db.findUserSettings(sessionUser.id);
+ const result: any = await db.findUserSettings();
// Returning null here when there are no settings so we can use the default settings with timezone from the browser
if (!result) return { settings: defaultUserSettings };
const settingsRes = userSettingsSchema.safeParse(result.settings);
if (!settingsRes.success) {
- ctx.c.executionCtx.waitUntil(db.updateUserSettings(sessionUser.id, defaultUserSettings));
+ ctx.c.executionCtx.waitUntil(db.updateUserSettings(defaultUserSettings));
console.log('returning default settings');
return { settings: defaultUserSettings };
}
@@ -35,13 +35,13 @@ export const settingsRouter = router({
save: privateProcedure.input(userSettingsSchema.partial()).mutation(async ({ ctx, input }) => {
const { sessionUser } = ctx;
const db = getZeroDB(sessionUser.id);
- const existingSettings: any = await db.findUserSettings(sessionUser.id);
+ const existingSettings: any = await db.findUserSettings();
if (existingSettings) {
const newSettings: any = { ...(existingSettings.settings as UserSettings), ...input };
- await db.updateUserSettings(sessionUser.id, newSettings);
+ await db.updateUserSettings(newSettings);
} else {
- await db.insertUserSettings(sessionUser.id, { ...(defaultUserSettings as any), ...input });
+ await db.insertUserSettings({ ...(defaultUserSettings as any), ...input });
}
return { success: true };
diff --git a/apps/server/src/trpc/routes/shortcut.ts b/apps/server/src/trpc/routes/shortcut.ts
index 6852b269f8..1cf34e48ee 100644
--- a/apps/server/src/trpc/routes/shortcut.ts
+++ b/apps/server/src/trpc/routes/shortcut.ts
@@ -14,6 +14,6 @@ export const shortcutRouter = router({
const { sessionUser } = ctx;
const { shortcuts } = input;
const db = getZeroDB(sessionUser.id);
- await db.insertUserHotkeys(sessionUser.id, shortcuts as any);
+ await db.insertUserHotkeys(shortcuts as any);
}),
});
diff --git a/apps/server/src/trpc/trpc.ts b/apps/server/src/trpc/trpc.ts
index 7a0cea5fc6..959d1d6b21 100644
--- a/apps/server/src/trpc/trpc.ts
+++ b/apps/server/src/trpc/trpc.ts
@@ -42,8 +42,7 @@ export const activeConnectionProcedure = privateProcedure.use(async ({ ctx, next
export const activeDriverProcedure = activeConnectionProcedure.use(async ({ ctx, next }) => {
const { activeConnection, sessionUser } = ctx;
- const driver = connectionToDriver(activeConnection);
- const res = await next({ ctx: { ...ctx, driver } });
+ const res = await next({ ctx: { ...ctx } });
// This is for when the user has not granted the required scopes for GMail
if (!res.ok && res.error.message === 'Precondition check failed.') {
diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts
index 95816afacf..b29d653092 100644
--- a/apps/server/src/types.ts
+++ b/apps/server/src/types.ts
@@ -129,13 +129,6 @@ export interface ParsedMessage {
isDraft?: boolean;
}
-export interface IConnection {
- id: string;
- email: string;
- name?: string;
- picture?: string;
-}
-
export interface Attachment {
attachmentId: string;
filename: string;
@@ -176,7 +169,13 @@ export interface IOutgoingMessage {
bcc?: Sender[];
subject: string;
message: string;
- attachments: File[];
+ attachments: {
+ name: string;
+ type: string;
+ size: number;
+ lastModified: number;
+ base64: string;
+ }[];
headers: Record;
threadId?: string;
fromEmail?: string;
@@ -215,6 +214,7 @@ export enum EPrompts {
SummarizeMessage = 'SummarizeMessage',
ReSummarizeThread = 'ReSummarizeThread',
SummarizeThread = 'SummarizeThread',
- // ThreadLabels = 'ThreadLabels',
- // Chat = 'Chat',
+ Chat = 'Chat',
+ Compose = 'Compose',
+ // ThreadLabels = 'ThreadLabels'
}
diff --git a/i18n.lock b/i18n.lock
index 0ebc026111..6a20de85d8 100644
--- a/i18n.lock
+++ b/i18n.lock
@@ -123,10 +123,13 @@ checksums:
common/searchBar/aiEnhancedQuery: 7bc20def675d9eacbec6d98a7f6cccd0
common/navUser/customerSupport: aa83f4456d11185e5e72dba3260bf00b
common/navUser/documentation: 1563fcb5ddb5037b0709ccd3dd384a92
- common/navUser/appTheme: bfaec6c6c45b8e7c28ea3b017260e2ce
+ common/navUser/appTheme: 0a3e19fea2f7b1045f53ebf0ccff6bd5
common/navUser/accounts: dbb9796c495f5e0f0de6f71bd32527ae
common/navUser/signIn: cb8757c7450e17de1e226e82fb0fa4a2
common/navUser/otherAccounts: 6206808c3673bd05c8d024eaac703f2a
+ common/navUser/switchingAccounts: fa53f8796a652161c8ec25281a1c5d3e
+ common/navUser/accountSwitched: e7b3f7c988574e8cf7aa8d2caed27adb
+ common/navUser/failedToSwitchAccount: 21c3b37b3b23c44637ce98b6af08e1c5
common/mailCategories/primary: f5fb269c8406736788b7785a0c68e500
common/mailCategories/allMail: 8ee9a794d1366b252decb770c6f7558a
common/mailCategories/important: 4cf0e8fc8e4e7c5c9b9458059a172a2b
@@ -240,6 +243,8 @@ checksums:
common/settings/saved: 0f46e6d1d0a3c93ca8136cc8c91fecd4
common/settings/failedToSave: 40417c48b29d285eadcd5363630dff1a
common/settings/languageChanged: 71fdc3486c49d228838ed1b541de999c
+ common/settings/defaultEmailUpdated: 9ef02980f416d937d5724dfc1434ba3f
+ common/settings/failedToUpdateGmailAlias: b8e186766156fba39688cd87ac8dc467
common/mail/replies: fc3e60cff29ffe474db270154bb64099
common/mail/deselectAll: d06b551a6d60cdc4dfc51c7683644bad
common/mail/selectedEmails: 8070ccaf317cb4216c364b2cb1223fd7
@@ -346,6 +351,9 @@ checksums:
pages/settings/general/noResultsFound: 5518f2865757dc73900aa03ef8be6934
pages/settings/general/zeroSignature: 0e94e29f00f4b39838ebed8a774148c3
pages/settings/general/zeroSignatureDescription: 296c4c81828361128b071969cde8ad76
+ pages/settings/general/defaultEmailAlias: dc269bad429dd22b27d82118441ea9b2
+ pages/settings/general/selectDefaultEmail: c2abb2947589920179e4757876ea905c
+ pages/settings/general/defaultEmailDescription: 0ebf26fceccb4cad99f4b2a20cbcfab0
pages/settings/connections/title: d7bc733cc82ab74c649a4816373a2295
pages/settings/connections/description: f05d355994f9fe0e089ce2c54d0de381
pages/settings/connections/disconnectTitle: efecc2354d665e68492fe985af575508
diff --git a/package.json b/package.json
index 4bf328a2ef..784bc4a16f 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "zero",
"version": "0.1.0",
"private": true,
- "packageManager": "pnpm@10.11.0",
+ "packageManager": "pnpm@10.12.1",
"scripts": {
"go": "pnpm docker:db:up && pnpm run dev",
"prepare": "husky",
diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts
index 0d313a87ba..0da70378e9 100644
--- a/packages/cli/src/utils.ts
+++ b/packages/cli/src/utils.ts
@@ -16,10 +16,21 @@ export const getProjectRoot = async () => {
};
export const runCommand = async (command: string, args: string[], options: SpawnOptions = {}) => {
- const child = spawn(command, args, { stdio: 'inherit', ...options });
- await new Promise((resolve, reject) => {
- child.once('close', resolve);
- child.once('error', reject);
+ const useShell = process.platform === 'win32';
+ const finalCommand = command;
+ const finalArgs = args;
+
+ const spawnOptions: SpawnOptions = {
+ stdio: 'inherit',
+ ...options,
+ ...(useShell && options.shell === undefined ? { shell: true } : {})
+ };
+
+ const child = spawn(finalCommand, finalArgs, spawnOptions);
+
+ await new Promise((resolve, reject) => {
+ child.once('close', () => resolve());
+ child.once('error', (err) => reject(err));
});
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 49eabaf006..fd4c2bdc2e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -154,6 +154,12 @@ importers:
'@tiptap/extension-document':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
+ '@tiptap/extension-file-handler':
+ specifier: 2.22.3
+ version: 2.22.3(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))
+ '@tiptap/extension-image':
+ specifier: 2.22.3
+ version: 2.22.3(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
'@tiptap/extension-link':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
@@ -569,6 +575,9 @@ importers:
jsonrepair:
specifier: ^3.12.0
version: 3.12.0
+ mime-types:
+ specifier: 3.0.1
+ version: 3.0.1
mimetext:
specifier: ^3.0.27
version: 3.0.27
@@ -602,6 +611,9 @@ importers:
twilio:
specifier: 5.7.0
version: 5.7.0
+ uuid:
+ specifier: 11.1.0
+ version: 11.1.0
wrangler:
specifier: 'catalog:'
version: 4.18.0(@cloudflare/workers-types@4.20250514.0)
@@ -621,6 +633,9 @@ importers:
'@types/sanitize-html':
specifier: 2.13.0
version: 2.13.0
+ '@types/uuid':
+ specifier: 10.0.0
+ version: 10.0.0
'@zero/eslint-config':
specifier: workspace:*
version: link:../../packages/eslint-config
@@ -3307,6 +3322,12 @@ packages:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
+ '@tiptap/extension-file-handler@2.22.3':
+ resolution: {integrity: sha512-SiIUUAWgYw6CO3OB5ADgOVEzeLQQR5sKyNzoccbM58Cy1Jy1gSYPKlRAPIlxmgHc98Ke8abcHqgt37p+s74o3A==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/extension-text-style': ^2.7.0
+
'@tiptap/extension-floating-menu@2.12.0':
resolution: {integrity: sha512-BYpyZx/56KCDksWuJJbhki/uNgt9sACuSSZFH5AN1yS1ISD+EzIxqf6Pzzv8QCoNJ+KcRNVaZsOlOFaJGoyzag==}
peerDependencies:
@@ -3346,8 +3367,8 @@ packages:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
- '@tiptap/extension-image@2.12.0':
- resolution: {integrity: sha512-wO+yrfMlnW3SYCb1Q1qAb+nt5WH6jnlQPTV6qdoIabRtW0puwMWULZDUgclPN5hxn8EXb9vBEu44egvH6hgkfQ==}
+ '@tiptap/extension-image@2.22.3':
+ resolution: {integrity: sha512-JO8n5YOqOs+bckPZZ3qJFFLpRbYlu4N52n/7Do0XmxEMWaa3fLcR0Rsa1v3X4dGH2T5cKQ475dWSpJQRc+x07w==}
peerDependencies:
'@tiptap/core': ^2.7.0
@@ -3600,6 +3621,9 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+ '@types/uuid@10.0.0':
+ resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
'@typescript-eslint/eslint-plugin@8.32.1':
resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -7309,6 +7333,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ uuid@11.1.0:
+ resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+ hasBin: true
+
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -10166,6 +10194,11 @@ snapshots:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/pm': 2.11.5
+ '@tiptap/extension-file-handler@2.22.3(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))':
+ dependencies:
+ '@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
+ '@tiptap/extension-text-style': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
+
'@tiptap/extension-floating-menu@2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)':
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
@@ -10199,7 +10232,7 @@ snapshots:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/pm': 2.11.5
- '@tiptap/extension-image@2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
+ '@tiptap/extension-image@2.22.3(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
@@ -10473,6 +10506,8 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
+ '@types/uuid@10.0.0': {}
+
'@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -13036,7 +13071,7 @@ snapshots:
'@tiptap/extension-color': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))
'@tiptap/extension-highlight': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
'@tiptap/extension-horizontal-rule': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
- '@tiptap/extension-image': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
+ '@tiptap/extension-image': 2.22.3(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
'@tiptap/extension-link': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
'@tiptap/extension-placeholder': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
'@tiptap/extension-task-item': 2.12.0(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
@@ -14832,6 +14867,8 @@ snapshots:
util-deprecate@1.0.2: {}
+ uuid@11.1.0: {}
+
uuid@9.0.1: {}
valibot@0.41.0(typescript@5.8.3):
diff --git a/turbo.json b/turbo.json
index 9c9b22e0df..52d0c5c2ab 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,5 +1,6 @@
{
"$schema": "https://turbo.build/schema.json",
+ "ui": "tui",
"envMode": "loose",
"tasks": {
"build": {