Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions apps/mail/app/api/driver/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,15 +833,44 @@ export const driver = async (config: IConfig): Promise<MailManager> => {
}
},
createDraft: async (data: any) => {
const mimeMessage = [
`From: me`,
`To: ${data.to}`,
`Subject: ${data.subject}`,
'Content-Type: text/html; charset=utf-8',
'',
data.message,
].join('\n');
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) {
const base64Data = await new Promise<string>((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);
});

msg.addAttachment({
filename: attachment.name,
contentType: attachment.type,
data: base64Data
});
}
}

const mimeMessage = msg.asRaw();
const encodedMessage = Buffer.from(mimeMessage)
.toString('base64')
.replace(/\+/g, '-')
Expand Down
22 changes: 18 additions & 4 deletions apps/mail/components/create/create-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -255,12 +267,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,
Expand Down
36 changes: 33 additions & 3 deletions apps/mail/components/draft/drafts-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,9 +20,16 @@ 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<ThreadProps, 'message'> {
message: ParsedMessage;
}

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);
Expand Down Expand Up @@ -51,7 +59,20 @@ const Draft = ({ message, onClick }: ThreadProps) => {
)}
>
<span className={cn(mail.selected && 'max-w-[120px] truncate')}>
{highlightText(message.sender.name, searchValue.highlight)}
{!message.to?.length ||
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,
)}
</span>
</p>
</div>
Expand All @@ -75,6 +96,15 @@ const Draft = ({ message, onClick }: ThreadProps) => {
>
{highlightText(message.subject, searchValue.highlight)}
</p>
<p
className={cn(
'mt-1 line-clamp-1 text-xs opacity-70 transition-opacity',
mail.selected ? 'line-clamp-1' : 'line-clamp-2',
isMailSelected && 'opacity-100',
)}
>
{highlightText(message.title || 'No content', searchValue.highlight)}
</p>
</div>
</div>
);
Expand Down