diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx
index 96bf7482c5..fd44c79fd1 100644
--- a/apps/mail/components/create/create-email.tsx
+++ b/apps/mail/components/create/create-email.tsx
@@ -10,6 +10,7 @@ import { EmailComposer } from './email-composer';
import { useSession } from '@/lib/auth-client';
import { serializeFiles } from '@/lib/schemas';
import { useDraft } from '@/hooks/use-drafts';
+import { useThreads } from '@/hooks/use-threads';
import { useEffect, useState } from 'react';
import type { Attachment } from '@/types';
@@ -54,6 +55,7 @@ export function CreateEmail({
data: draft,
isLoading: isDraftLoading,
error: draftError,
+ refetch: refetchDraft
} = useDraft(draftId ?? propDraftId ?? null);
const [, setIsDraftFailed] = useState(false);
@@ -61,6 +63,7 @@ export function CreateEmail({
const { mutateAsync: sendEmail } = useMutation(trpc.mail.send.mutationOptions());
const [isComposeOpen, setIsComposeOpen] = useQueryState('isComposeOpen');
const [, setThreadId] = useQueryState('threadId');
+ const [{refetch: refetchThreads }] = useThreads();
const [, setActiveReplyId] = useQueryState('activeReplyId');
const { data: activeConnection } = useActiveConnection();
const { data: settings, isLoading: settingsLoading } = useSettings();
@@ -144,6 +147,8 @@ export function CreateEmail({
}
};
+
+
const base64ToFile = (base64: string, filename: string, mimeType: string): File | null => {
try {
const byteString = atob(base64);
@@ -169,7 +174,12 @@ export function CreateEmail({
-
- {isDraftLoading ? (
-
- ) : (
{
+ refetchDraft();
+ }}
initialAttachments={files}
initialSubject={typedDraft?.subject || initialSubject}
autofocus={false}
settingsLoading={settingsLoading}
/>
- )}
>
diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx
index 6a10415f12..fbda364149 100644
--- a/apps/mail/components/create/email-composer.tsx
+++ b/apps/mail/components/create/email-composer.tsx
@@ -15,7 +15,7 @@ import {
} from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
-import { Check, Command, Loader, Paperclip, Plus, Type, X as XIcon } from 'lucide-react';
+import { Check, Command, Loader, Paperclip, Trash, Plus, Type, X as XIcon } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { TextEffect } from '@/components/motion-primitives/text-effect';
import { ImageCompressionSettings } from './image-compression-settings';
@@ -35,7 +35,7 @@ import { useMutation } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';
import { cn, formatFileSize } from '@/lib/utils';
-import { useThread } from '@/hooks/use-threads';
+import { useThread, useThreads } from '@/hooks/use-threads';
import { serializeFiles } from '@/lib/schemas';
import { Input } from '@/components/ui/input';
import { EditorContent } from '@tiptap/react';
@@ -75,6 +75,7 @@ interface EmailComposerProps {
fromEmail?: string;
}) => Promise;
onClose?: () => void;
+ onDraftUpdate?: () => void;
className?: string;
autofocus?: boolean;
settingsLoading?: boolean;
@@ -112,6 +113,8 @@ export function EmailComposer({
initialAttachments = [],
onSendEmail,
onClose,
+ onDraftUpdate,
+ // onDeleteDrafts,
className,
autofocus = false,
settingsLoading = false,
@@ -123,6 +126,8 @@ export function EmailComposer({
const [showBcc, setShowBcc] = useState(initialBcc.length > 0);
const [isLoading, setIsLoading] = useState(false);
const [isSavingDraft, setIsSavingDraft] = useState(false);
+ const [isDeleteDraft, setIsDeleteDraft] = useState(false);
+ const [{ isFetching, refetch: refetchThreads }] = useThreads();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [messageLength, setMessageLength] = useState(0);
const fileInputRef = useRef(null);
@@ -253,6 +258,8 @@ export function EmailComposer({
const trpc = useTRPC();
const { mutateAsync: aiCompose } = useMutation(trpc.ai.compose.mutationOptions());
const { mutateAsync: createDraft } = useMutation(trpc.drafts.create.mutationOptions());
+ const {mutateAsync: updateDraft} = useMutation(trpc.drafts.update.mutationOptions());
+ const {mutateAsync: deleteDraft} = useMutation(trpc.drafts.delete.mutationOptions());
const { mutateAsync: generateEmailSubject } = useMutation(
trpc.ai.generateEmailSubject.mutationOptions(),
);
@@ -553,6 +560,8 @@ export function EmailComposer({
}
};
+
+
const saveDraft = async () => {
const values = getValues();
@@ -578,11 +587,37 @@ export function EmailComposer({
fromEmail: values.fromEmail ? values.fromEmail : null,
};
- const response = await createDraft(draftData);
-
- if (response?.id && response.id !== draftId) {
- setDraftId(response.id);
- }
+ if(draftId){
+ try{
+ const response = await updateDraft(draftData);
+ if(response?.id){
+ setDraftId(response?.id);
+ onDraftUpdate?.();
+ toast.success("Your Draft has been Successfully Saved")
+ }
+ else{
+ console.error("Failed Setting up Draft Id")
+ toast.error("Failed Setting up Draft Id")
+ }
+ } catch(error){
+ console.error("Failed to create draft:",error)
+ toast.error("Failed to create draft")
+ }
+ } else {
+ try{
+ const response = await createDraft(draftData);
+ if(response?.id){
+ setDraftId(response?.id);
+ toast.success("Your Draft has been Successfully Saved")
+ } else{
+ console.error("Failed Setting up Draft Id")
+ toast.error("Failed Setting up Draft Id")
+ }
+ }catch (error) {
+ console.error("Failed to create draft:", error);
+ toast.error("Failed to create draft");
+ }
+ }
} catch (error) {
console.error('Error saving draft:', error);
toast.error('Failed to save draft');
@@ -594,6 +629,48 @@ export function EmailComposer({
}
};
+ useEffect(() => {
+ if (!hasUnsavedChanges) return;
+
+ const autoSaveTimer = setTimeout(() => {
+ saveDraft();
+ }, 3000);
+
+ return () => clearTimeout(autoSaveTimer);
+ }, [hasUnsavedChanges]);
+
+
+ const handleDeleteDraft = async () => {
+ if (!draftId) {
+ toast.error('No draft Id available to delete the draft.');
+ return;
+ }
+ try{
+ const response = await deleteDraft({id: draftId});
+ if(response === ''){
+ setDraftId(null);
+ setIsComposeOpen(null);
+ setTimeout(() => {
+ toast.success("Successfully Deleted Draft");
+ refetchThreads();
+ }, 500);
+ }
+ } catch (error) {
+ console.error('Failed to delete draft:', error);
+ toast.error('Failed to delete draft.');
+ }
+ };
+ // this handleclose button triggeres to auto-save draft upon close
+ const handleClose = () => {
+ const hasContent = editor?.getText()?.trim().length > 0;
+ if (hasContent) {
+ saveDraft();
+ setShowLeaveConfirmation(true);
+ } else {
+ onClose?.();
+ }
+ };
+
const handleGenerateSubject = async () => {
try {
setIsGeneratingSubject(true);
@@ -615,17 +692,10 @@ export function EmailComposer({
}
};
- const handleClose = () => {
- const hasContent = editor?.getText()?.trim().length > 0;
- if (hasContent) {
- setShowLeaveConfirmation(true);
- } else {
- onClose?.();
- }
- };
const confirmLeave = () => {
setShowLeaveConfirmation(false);
+ handledeleteDraft();
onClose?.();
};
@@ -646,16 +716,6 @@ export function EmailComposer({
};
}, [editor, showLeaveConfirmation]);
- useEffect(() => {
- if (!hasUnsavedChanges) return;
-
- const autoSaveTimer = setTimeout(() => {
- console.log('timeout set');
- saveDraft();
- }, 3000);
-
- return () => clearTimeout(autoSaveTimer);
- }, [hasUnsavedChanges, saveDraft]);
useEffect(() => {
const handlePasteFiles = (event: ClipboardEvent) => {
@@ -923,7 +983,7 @@ export function EmailComposer({
tabIndex={-1}
className="flex h-full items-center gap-2 text-sm font-medium text-[#8C8C8C] hover:text-[#A8A8A8]"
onClick={handleClose}
- >
+ >
)}
@@ -1480,7 +1540,7 @@ export function EmailComposer({
-
+
{aiGeneratedMessage !== null ? (
@@ -1579,7 +1639,7 @@ export function EmailComposer({
Stay
- Leave
+ Discard
diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts
index 9d9a285e25..f4127d966d 100644
--- a/apps/server/src/lib/driver/google.ts
+++ b/apps/server/src/lib/driver/google.ts
@@ -682,7 +682,7 @@ export class GoogleMailManager implements MailManager {
const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
const msg = createMimeMessage();
msg.setSender('me');
- // name
+
const to = data.to.split(', ').map((recipient: string) => {
if (recipient.includes('<')) {
const [name, email] = recipient.split('<');
@@ -692,12 +692,10 @@ export class GoogleMailManager implements MailManager {
});
msg.setTo(to);
- if (data.cc)
- msg.setCc(data.cc?.split(', ').map((recipient: string) => ({ addr: recipient })));
- if (data.bcc)
- msg.setBcc(data.bcc?.split(', ').map((recipient: string) => ({ addr: recipient })));
-
+ if (data.cc) msg.setCc(data.cc.split(', ').map((addr) => ({ addr })));
+ if (data.bcc) msg.setBcc(data.bcc.split(', ').map((addr) => ({ addr })));
msg.setSubject(data.subject);
+
msg.addMessage({
contentType: 'text/html',
data: message || '',
@@ -718,9 +716,9 @@ export class GoogleMailManager implements MailManager {
}
}
- if (data.attachments && data.attachments?.length > 0) {
+ if (data.attachments?.length) {
for (const attachment of data.attachments) {
- const base64Data = attachment.base64;
+ const base64Data = await attachment.base64;
msg.addAttachment({
filename: attachment.name,
contentType: attachment.type,
@@ -743,24 +741,106 @@ export class GoogleMailManager implements MailManager {
},
};
- let res;
+ const res = await this.gmail.users.drafts.create({
+ userId: 'me',
+ requestBody,
+ });
- if (data.id) {
- res = await this.gmail.users.drafts.update({
- userId: 'me',
- id: data.id,
- requestBody,
- });
- } else {
- res = await this.gmail.users.drafts.create({
- userId: 'me',
- requestBody,
- });
+ return res.data;
+ },
+ { data }
+ );
+ }
+ public updateDraft(data: CreateDraftData) {
+ return this.withErrorHandler(
+ 'updateDraft',
+ async () => {
+ if (!data.id) throw new Error('Missing draft ID for update');
+
+ const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
+ const msg = createMimeMessage();
+ msg.setSender('me');
+
+ const to = data.to.split(', ').map((recipient: string) => {
+ if (recipient.includes('<')) {
+ const [name, email] = recipient.split('<');
+ return { addr: email.replace('>', ''), name: name.replace('>', '') };
+ }
+ return { addr: recipient };
+ });
+
+ msg.setTo(to);
+ if (data.cc) msg.setCc(data.cc.split(', ').map((addr) => ({ addr })));
+ if (data.bcc) msg.setBcc(data.bcc.split(', ').map((addr) => ({ addr })));
+ msg.setSubject(data.subject);
+
+ msg.addMessage({
+ contentType: 'text/html',
+ 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?.length) {
+ for (const attachment of data.attachments) {
+ const base64Data = attachment.base64;
+ msg.addAttachment({
+ filename: attachment.name,
+ contentType: attachment.type,
+ data: base64Data,
+ });
+ }
}
+ const mimeMessage = msg.asRaw();
+ const encodedMessage = Buffer.from(mimeMessage)
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+
+ const requestBody = {
+ message: {
+ raw: encodedMessage,
+ threadId: data.threadId,
+ },
+ };
+
+ const res = await this.gmail.users.drafts.update({
+ userId: 'me',
+ id: data.id,
+ requestBody,
+ });
+
return res.data;
},
- { data },
+ { data }
+ );
+ }
+ public deleteDraft(draftId: string){
+ return this.withErrorHandler(
+ 'deleteDraft',
+ async () =>{
+ if (!draftId) throw new Error('Missing draft ID to delete');
+
+ const res = await this.gmail.users.drafts.delete({
+ userId: 'me',
+ id: draftId,
+ })
+ return res.data;
+ } , {draftId}
);
}
public async getUserLabels() {
diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts
index bacce02bbd..7ba68bea5f 100644
--- a/apps/server/src/lib/driver/microsoft.ts
+++ b/apps/server/src/lib/driver/microsoft.ts
@@ -636,7 +636,6 @@ export class OutlookMailManager implements MailManager {
const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', ');
-
const outlookMessage: Message = {
subject: data.subject,
body: {
@@ -707,35 +706,84 @@ export class OutlookMailManager implements MailManager {
if (allAttachments.length > 0) {
outlookMessage.attachments = allAttachments;
}
+ const res = await this.graphClient
+ .api('/me/mailfolders/drafts/messages')
+ .post(outlookMessage);
- let res;
+ return res;
+ },
+ { data },
+ );
+ }
+ public updateDraft(data: CreateDraftData) {
+ return this.withErrorHandler(
+ 'updateDraft',
+ async () => {
+ if (!data.id) throw new Error('Draft ID is required to update a draft');
- if (data.id) {
- try {
- res = await this.graphClient
- .api(`/me/mailfolders/drafts/messages/${data.id}`)
- .patch(outlookMessage);
- } catch (error) {
- console.warn(`Failed to update draft ${data.id}, creating a new one`, error);
- try {
- await this.graphClient.api(`/me/mailfolders/drafts/messages/${data.id}`).delete();
- } catch (deleteError) {
- console.error(`Failed to delete draft ${data.id}`, deleteError);
- }
+ const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);
- res = await this.graphClient
- .api('/me/mailfolders/drafts/messages')
- .post(outlookMessage);
- }
- } else {
- res = await this.graphClient.api('/me/mailfolders/drafts/messages').post(outlookMessage);
+ const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', ');
+ const outlookMessage: Message = {
+ subject: data.subject,
+ body: {
+ contentType: 'html',
+ content: message || '',
+ },
+ toRecipients: toRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ })),
+ };
+
+ if (data.cc) {
+ const ccRecipients = Array.isArray(data.cc) ? data.cc : data.cc.split(', ');
+ outlookMessage.ccRecipients = ccRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ }));
+ }
+
+ if (data.bcc) {
+ const bccRecipients = Array.isArray(data.bcc) ? data.bcc : data.bcc.split(', ');
+ outlookMessage.bccRecipients = bccRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ }));
}
+ if (data.attachments && data.attachments.length > 0) {
+ outlookMessage.attachments = await Promise.all(
+ data.attachments.map(async (file) => {
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ const base64Content = buffer.toString('base64');
+
+ return {
+ '@odata.type': '#microsoft.graph.fileAttachment',
+ name: file.name,
+ contentType: file.type || 'application/octet-stream',
+ contentBytes: base64Content,
+ };
+ }),
+ );
+ }
+ const res = await this.graphClient
+ .api(`/me/mailfolders/drafts/messages/${data.id}`)
+ .patch(outlookMessage);
+
return res;
},
{ data },
);
}
+
public async getUserLabels() {
try {
// Get root mail folders
diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts
index 48fd7354a1..ab83e3aa28 100644
--- a/apps/server/src/lib/driver/types.ts
+++ b/apps/server/src/lib/driver/types.ts
@@ -68,6 +68,12 @@ export interface MailManager {
createDraft(
data: CreateDraftData,
): Promise<{ id?: string | null; success?: boolean; error?: string }>;
+ updateDraft(
+ data: CreateDraftData
+ ): Promise<{ id?: string | null, success?: boolean, error?: string}>;
+ deleteDraft(
+ draftId: string
+ ): Promise;
getDraft(id: string): Promise;
listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }): Promise<{
threads: { id: string; historyId: string | null; $raw: unknown }[];
diff --git a/apps/server/src/routes/agent/index.ts b/apps/server/src/routes/agent/index.ts
index 39b63efff8..ca2b60cddb 100644
--- a/apps/server/src/routes/agent/index.ts
+++ b/apps/server/src/routes/agent/index.ts
@@ -781,6 +781,20 @@ export class ZeroDriver extends Agent {
return await this.driver.createDraft(draftData);
}
+ async updateDraft( draftData: CreateDraftData){
+ if(!this.driver){
+ throw new Error('No driver available')
+ }
+ return await this.driver.updateDraft(draftData);
+ }
+
+ async deleteDraft(id: string){
+ if(!this.driver){
+ throw new Error('No driver available')
+ }
+ return await this.driver.deleteDraft(id);
+ }
+
async getDraft(id: string) {
if (!this.driver) {
throw new Error('No driver available');
diff --git a/apps/server/src/routes/agent/rpc.ts b/apps/server/src/routes/agent/rpc.ts
index 93c8665cd9..010c1efe80 100644
--- a/apps/server/src/routes/agent/rpc.ts
+++ b/apps/server/src/routes/agent/rpc.ts
@@ -125,6 +125,14 @@ export class DriverRpcDO extends RpcTarget {
return await this.mainDo.createDraft(draftData);
}
+ async updateDraft(draftData: CreateDraftData){
+ return await this.mainDo.updateDraft(draftData);
+ }
+
+ async deleteDraft(draftId: string){
+ return await this.mainDo.deleteDraft(draftId);
+ }
+
async getDraft(id: string) {
return await this.mainDo.getDraft(id);
}
diff --git a/apps/server/src/trpc/routes/drafts.ts b/apps/server/src/trpc/routes/drafts.ts
index 144f8d7f03..fd79214b7a 100644
--- a/apps/server/src/trpc/routes/drafts.ts
+++ b/apps/server/src/trpc/routes/drafts.ts
@@ -10,11 +10,27 @@ export const draftsRouter = router({
const agent = await getZeroAgent(activeConnection.id);
return agent.createDraft(input);
}),
+ update: activeDriverProcedure
+ .input(createDraftData)
+ .mutation(async ({input, ctx}) =>{
+ const {activeConnection} = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ const res = await agent.updateDraft(input);
+ return res;
+ }),
+ delete: activeDriverProcedure
+ .input(z.object({ id: z.string() }))
+ .mutation(async ({ input, ctx }) => {
+ const { activeConnection } = ctx;
+ const agent = await getZeroAgent(activeConnection.id);
+ return agent.deleteDraft(input.id);
+ }),
get: activeDriverProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
const { activeConnection } = ctx;
const agent = await getZeroAgent(activeConnection.id);
const { id } = input;
- return agent.getDraft(id) as ReturnType;
+ const res = await agent.getDraft(id) as Awaited>
+ return res;
}),
list: activeDriverProcedure
.input(
@@ -28,8 +44,7 @@ export const draftsRouter = router({
const { activeConnection } = ctx;
const agent = await getZeroAgent(activeConnection.id);
const { q, maxResults, pageToken } = input;
- return agent.listDrafts({ q, maxResults, pageToken }) as Awaited<
- ReturnType
- >;
+ const res = agent.listDrafts({ q, maxResults: maxResults, pageToken }) as Awaited>;
+ return res;
}),
});