From 1a0ad907096e73a012343f486610cfaaf0ee1942 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 01:28:44 +0100 Subject: [PATCH 1/8] draft fixes: - added cc and bcc when saving drafts - save drafts less aggresively --- apps/mail/components/create/create-email.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 753a586dce..624ac687c8 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -255,12 +255,14 @@ export function CreateEmail({ const saveDraft = React.useCallback(async () => { if (!hasUnsavedChanges) return; - if (!toEmails.length && !subjectInput && !messageContent) return; + if (!toEmails.length || !subjectInput || !messageContent) return; try { setIsLoading(true); const draftData = { to: toEmails.join(', '), + cc: ccEmails.join(', '), + bcc: bccEmails.join(', '), subject: subjectInput, message: messageContent || '', attachments: attachments, From 1622ccabb8450b2a8053bee79660c2f97e9ce935 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 02:29:07 +0100 Subject: [PATCH 2/8] some fixes for saving attachments to draft --- apps/mail/app/api/driver/google.ts | 49 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts index 73d39d00cb..4215a55fbb 100644 --- a/apps/mail/app/api/driver/google.ts +++ b/apps/mail/app/api/driver/google.ts @@ -824,14 +824,57 @@ export const driver = async (config: IConfig): Promise => { } }, createDraft: async (data: any) => { - const mimeMessage = [ + // Generate a unique boundary for multipart messages + const boundary = `boundary_${Date.now()}`; + + // Start building MIME message parts + const messageParts = [ `From: me`, `To: ${data.to}`, + data.cc ? `Cc: ${data.cc}` : '', + data.bcc ? `Bcc: ${data.bcc}` : '', `Subject: ${data.subject}`, + `MIME-Version: 1.0`, + `Content-Type: multipart/mixed; boundary=${boundary}`, + '', + `--${boundary}`, 'Content-Type: text/html; charset=utf-8', '', - data.message, - ].join('\n'); + data.message || '', + ]; + + // Add attachments if present + if (data.attachments?.length > 0) { + for (const attachment of data.attachments) { + const base64Data = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64 = (reader.result as string).split(',')[1]; + if (base64) { + resolve(base64); + } else { + reject(new Error('Failed to read file as base64')); + } + }; + reader.onerror = () => reject(new Error('Failed to read file')); + reader.readAsDataURL(attachment); + }); + + messageParts.push( + `--${boundary}`, + `Content-Type: ${attachment.type}`, + `Content-Transfer-Encoding: base64`, + `Content-Disposition: attachment; filename="${attachment.name}"`, + '', + base64Data, + ); + } + } + + // Close the multipart message + messageParts.push(`--${boundary}--`); + + const mimeMessage = messageParts.filter(Boolean).join('\n'); const encodedMessage = Buffer.from(mimeMessage) .toString('base64') From eea882722f492a572a814e60b70bab2f03a62d8f Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 02:30:52 +0100 Subject: [PATCH 3/8] fix for empty draft loading --- apps/mail/components/create/create-email.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 624ac687c8..ee0df71062 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -173,12 +173,24 @@ export function CreateEmail({ if (draft.content) { try { - const json = generateJSON(draft.content, [Document, Paragraph, Text, Bold]); - setDefaultValue(json); setMessageContent(draft.content); + setResetEditorKey((prev) => prev + 1); + setTimeout(() => { + try { + const json = generateJSON(draft.content, [Document, Paragraph, Text, Bold]); + setDefaultValue(json); + } catch (error) { + console.error('Error parsing draft content:', error); + setDefaultValue(createEmptyDocContent()); + } + }, 0); } catch (error) { - console.error('Error parsing draft content:', error); + console.error('Error setting draft content:', error); + setDefaultValue(createEmptyDocContent()); } + } else { + setDefaultValue(createEmptyDocContent()); + setMessageContent(''); } setHasUnsavedChanges(false); From 3a2860eaaa33d7532a0d22f3e429363b79e98414 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 13:48:03 +0100 Subject: [PATCH 4/8] fix draft list recipient name/address --- apps/mail/components/draft/drafts-list.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/mail/components/draft/drafts-list.tsx b/apps/mail/components/draft/drafts-list.tsx index a659058218..48a1a54136 100644 --- a/apps/mail/components/draft/drafts-list.tsx +++ b/apps/mail/components/draft/drafts-list.tsx @@ -19,7 +19,13 @@ import { ChevronDown } from 'lucide-react'; import { Button } from '../ui/button'; import { toast } from 'sonner'; -const Draft = ({ message, onClick }: ThreadProps) => { +import { ParsedMessage } from '@/types'; + +interface DraftProps extends Omit { + message: ParsedMessage; +} + +const Draft = ({ message, onClick }: DraftProps) => { const [mail] = useMail(); const [searchValue] = useSearchValue(); @@ -51,7 +57,19 @@ const Draft = ({ message, onClick }: ThreadProps) => { )} > - {highlightText(message.sender.name, searchValue.highlight)} + {message.to.some( + (to: { name: string; email: string }) => + to.name.includes('no-sender') || to.email.includes('no-sender'), + ) + ? 'No recipient' + : highlightText( + message.to + .map((to: { name: string; email: string }) => + to.name === 'No Sender Name' ? to.email : `${to.name} <${to.email}>`, + ) + .join(', '), + searchValue.highlight, + )}

From 397090cab97ecc7f3e81c70972775501c113d4c2 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 13:49:47 +0100 Subject: [PATCH 5/8] also show 'No Recipient' if empty --- apps/mail/components/draft/drafts-list.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/draft/drafts-list.tsx b/apps/mail/components/draft/drafts-list.tsx index 48a1a54136..89c84332ed 100644 --- a/apps/mail/components/draft/drafts-list.tsx +++ b/apps/mail/components/draft/drafts-list.tsx @@ -57,7 +57,8 @@ const Draft = ({ message, onClick }: DraftProps) => { )} > - {message.to.some( + {!message.to?.length || + message.to.some( (to: { name: string; email: string }) => to.name.includes('no-sender') || to.email.includes('no-sender'), ) From cc52f7362d30929bd482dad974689f72fc770623 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 19 Apr 2025 14:37:00 +0100 Subject: [PATCH 6/8] remove comments --- apps/mail/app/api/driver/google.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts index 4215a55fbb..a5166390dc 100644 --- a/apps/mail/app/api/driver/google.ts +++ b/apps/mail/app/api/driver/google.ts @@ -824,10 +824,8 @@ export const driver = async (config: IConfig): Promise => { } }, createDraft: async (data: any) => { - // Generate a unique boundary for multipart messages const boundary = `boundary_${Date.now()}`; - // Start building MIME message parts const messageParts = [ `From: me`, `To: ${data.to}`, @@ -843,7 +841,6 @@ export const driver = async (config: IConfig): Promise => { data.message || '', ]; - // Add attachments if present if (data.attachments?.length > 0) { for (const attachment of data.attachments) { const base64Data = await new Promise((resolve, reject) => { @@ -871,7 +868,6 @@ export const driver = async (config: IConfig): Promise => { } } - // Close the multipart message messageParts.push(`--${boundary}--`); const mimeMessage = messageParts.filter(Boolean).join('\n'); From 0214edbe552e3c625bbd17eddfe69f8097f74a15 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Mon, 21 Apr 2025 02:19:46 +0100 Subject: [PATCH 7/8] switch to mimetext for draft saving to keep formatting consistent --- apps/mail/app/api/driver/google.ts | 46 ++++++++++++------------------ 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts index 4902777eaf..60842fd8a7 100644 --- a/apps/mail/app/api/driver/google.ts +++ b/apps/mail/app/api/driver/google.ts @@ -828,22 +828,18 @@ export const driver = async (config: IConfig): Promise => { } }, createDraft: async (data: any) => { - const boundary = `boundary_${Date.now()}`; - - const messageParts = [ - `From: me`, - `To: ${data.to}`, - data.cc ? `Cc: ${data.cc}` : '', - data.bcc ? `Bcc: ${data.bcc}` : '', - `Subject: ${data.subject}`, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary=${boundary}`, - '', - `--${boundary}`, - 'Content-Type: text/html; charset=utf-8', - '', - data.message || '', - ]; + const msg = createMimeMessage(); + msg.setSender('me'); + msg.setTo(data.to); + + if (data.cc) msg.setCc(data.cc); + if (data.bcc) msg.setBcc(data.bcc); + + msg.setSubject(data.subject); + msg.addMessage({ + contentType: 'text/html', + data: data.message || '' + }); if (data.attachments?.length > 0) { for (const attachment of data.attachments) { @@ -861,21 +857,15 @@ export const driver = async (config: IConfig): Promise => { reader.readAsDataURL(attachment); }); - messageParts.push( - `--${boundary}`, - `Content-Type: ${attachment.type}`, - `Content-Transfer-Encoding: base64`, - `Content-Disposition: attachment; filename="${attachment.name}"`, - '', - base64Data, - ); + msg.addAttachment({ + filename: attachment.name, + contentType: attachment.type, + data: base64Data + }); } } - messageParts.push(`--${boundary}--`); - - const mimeMessage = messageParts.filter(Boolean).join('\n'); - + const mimeMessage = msg.asRaw(); const encodedMessage = Buffer.from(mimeMessage) .toString('base64') .replace(/\+/g, '-') From 3253ec378fbfb633b3768df1d345e6ef55d05953 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Mon, 21 Apr 2025 02:48:23 +0100 Subject: [PATCH 8/8] add message title to draft list --- apps/mail/components/draft/drafts-list.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/draft/drafts-list.tsx b/apps/mail/components/draft/drafts-list.tsx index 89c84332ed..114161d488 100644 --- a/apps/mail/components/draft/drafts-list.tsx +++ b/apps/mail/components/draft/drafts-list.tsx @@ -2,9 +2,10 @@ import type { InitialThread, ThreadProps, MailListProps, MailSelectMode } from '@/types'; import { EmptyState, type FolderType } from '@/components/mail/empty-state'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'; import { cn, defaultPageSize, formatDate } from '@/lib/utils'; +import { extractTextFromHTML } from '@/actions/extractText'; import { useSearchValue } from '@/hooks/use-search-value'; import { markAsRead, markAsUnread } from '@/actions/mail'; import { highlightText } from '@/lib/email-utils.client'; @@ -28,6 +29,7 @@ interface DraftProps extends Omit { const Draft = ({ message, onClick }: DraftProps) => { const [mail] = useMail(); const [searchValue] = useSearchValue(); + const [bodyText, setBodyText] = React.useState(''); const isMailSelected = message.id === mail.selected; const isMailBulkSelected = mail.bulkSelected.includes(message.id); @@ -94,6 +96,15 @@ const Draft = ({ message, onClick }: DraftProps) => { > {highlightText(message.subject, searchValue.highlight)}

+

+ {highlightText(message.title || 'No content', searchValue.highlight)} +

);