diff --git a/apps/mail/actions/send.ts b/apps/mail/actions/send.ts index e7733da7f..bd99f032d 100644 --- a/apps/mail/actions/send.ts +++ b/apps/mail/actions/send.ts @@ -6,22 +6,24 @@ import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { eq } from "drizzle-orm"; import { db } from "@zero/db"; -import { getUserSettings } from "./settings"; +import { Sender } from "@/types"; export async function sendEmail({ to, subject, message, attachments, + bcc, + cc, headers: additionalHeaders = {}, - includeSignature = true, }: { - to: string; + to: Sender[]; subject: string; message: string; attachments: File[]; - headers?: Record; - includeSignature?: boolean; + headers?: Record; + cc?: Sender[]; + bcc?: Sender[]; }) { if (!to || !subject || !message) { throw new Error("Missing required fields"); @@ -42,134 +44,22 @@ export async function sendEmail({ throw new Error("Unauthorized, reconnect"); } - // Get user settings to check for signature - let finalMessage = message.trim(); - - // Create the email HTML structure with optional signature - const htmlTemplate = (content: string, signature?: string) => ` - - - - - - - -${content} -${signature ? ` -
-${signature} -
` : ''} - -`; - - if (includeSignature) { - const userSettings = await getUserSettings(); - if (userSettings?.signature?.enabled && userSettings.signature.content) { - const signatureContent = userSettings.signature.content.trim(); - finalMessage = htmlTemplate(finalMessage, signatureContent); - } else { - finalMessage = htmlTemplate(finalMessage); - } - } else { - finalMessage = htmlTemplate(finalMessage); - } - const driver = await createDriver(_connection.providerId, { auth: { access_token: _connection.accessToken, refresh_token: _connection.refreshToken, + email: _connection.email }, }); - const fromName = _connection.name || session.user.name || "Unknown"; - const fromEmail = _connection.email || session.user.email; - const fromHeader = fromName ? `${fromName} <${fromEmail}>` : fromEmail; - - const domain = fromEmail.split("@")[1]; - const randomPart = Math.random().toString(36).substring(2); - const timestamp = Date.now(); - const messageId = `<${timestamp}.${randomPart}.zerodotemail@${domain}>`; - - const date = new Date().toUTCString(); - const boundary = `----=_NextPart_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`; - - // Start building email content - const emailParts = [ - `Content-Type: multipart/mixed; boundary="${boundary}"`, - "MIME-Version: 1.0", - `Date: ${date}`, - `Message-ID: ${messageId}`, - `From: ${fromHeader}`, - `To: ${to - .split(",") - .map((ref) => (ref.startsWith("<") ? ref : `<${ref}>`)) - .join(", ")}`, - `Subject: ${subject}`, - `X-Mailer: 0.email`, - `X-Priority: 3`, - `X-MSMail-Priority: Normal`, - - // Add threading headers if present - ...(additionalHeaders["In-Reply-To"] - ? [ - `In-Reply-To: ${additionalHeaders["In-Reply-To"] - .split(" ") - .filter(Boolean) - .map((ref) => (ref.startsWith("<") ? ref : `<${ref}>`)) - .join(" ")}`, - ] - : []), - ...(additionalHeaders["References"] - ? [ - `References: ${additionalHeaders["References"] - .split(" ") - .filter(Boolean) - .map((ref) => (ref.startsWith("<") ? ref : `<${ref}>`)) - .join(" ")}`, - ] - : []), - - // Security headers - `X-Originating-IP: [PRIVATE]`, - `Importance: Normal`, - "", - `--${boundary}`, - "Content-Type: text/html; charset=UTF-8", - "Content-Transfer-Encoding: 8bit", - "Content-Disposition: inline", - "", - finalMessage.trim(), - ]; - - // Process attachments if any - if (attachments?.length > 0) { - for (const file of attachments) { - // Convert File to ArrayBuffer - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const base64Content = buffer.toString("base64"); - - emailParts.push( - `--${boundary}`, - `Content-Type: ${file.type || "application/octet-stream"}`, - `Content-Transfer-Encoding: base64`, - `Content-Disposition: attachment; filename="${file.name}"`, - "", - base64Content.match(/.{1,76}/g)?.join("\n") || base64Content, - ); - } - } - - // Add final boundary - emailParts.push(`--${boundary}--`); - - // Join all parts with CRLF - const emailContent = emailParts.join("\r\n"); - - const encodedMessage = Buffer.from(emailContent).toString("base64"); - await driver.create({ - raw: encodedMessage, + subject, + to, + message, + attachments, + headers: additionalHeaders, + cc, + bcc }); return { success: true }; diff --git a/apps/mail/app/(routes)/settings/signatures/page.tsx b/apps/mail/app/(routes)/settings/signatures/page.tsx index cc0b3af76..92fbccb81 100644 --- a/apps/mail/app/(routes)/settings/signatures/page.tsx +++ b/apps/mail/app/(routes)/settings/signatures/page.tsx @@ -1,5 +1,5 @@ 'use client'; - +// DEPRECATED - import { Form, FormControl, diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts index 8f808040e..0e7e9ff1b 100644 --- a/apps/mail/app/api/driver/google.ts +++ b/apps/mail/app/api/driver/google.ts @@ -2,8 +2,9 @@ import { parseAddressList, parseFrom, wasSentWithTLS } from '@/lib/email-utils'; import { type IConfig, type MailManager } from './types'; import { type gmail_v1, google } from 'googleapis'; import { EnableBrain } from '@/actions/brain'; -import { type ParsedMessage } from '@/types'; +import { IOutgoingMessage, Sender, type ParsedMessage } from '@/types'; import * as he from 'he'; +import { createMimeMessage } from 'mimetext'; function fromBase64Url(str: string) { return str.replace(/-/g, '+').replace(/_/g, '/'); @@ -141,7 +142,12 @@ export const driver = async (config: IConfig): Promise => { ?.filter((h) => h.name?.toLowerCase() === 'cc') .map((h) => h.value) .filter((v) => typeof v === 'string') || []; - const cc = ccHeaders.flatMap((to) => parseAddressList(to)); + + const cc = ccHeaders.length > 0 + ? ccHeaders + .filter(header => header.trim().length > 0) + .flatMap(header => parseAddressList(header)) + : null; const receivedHeaders = payload?.headers @@ -170,6 +176,62 @@ export const driver = async (config: IConfig): Promise => { messageId, }; }; + const parseOutgoing = async ({ to, subject, message, attachments, headers, cc, bcc }: IOutgoingMessage) => { + const msg = createMimeMessage(); + + const fromEmail = config.auth?.email || 'nobody@example.com'; + msg.setSender(fromEmail); + + to.forEach(recipient => { + msg.setRecipient(({ + addr: recipient.email, + name: recipient.name + })); + }); + + if (cc) msg.setCc(cc.map(recipient => ({ + addr: recipient.email, + name: recipient.name + }))); + if (bcc) msg.setBcc(bcc.map(recipient => ({ + addr: recipient.email, + name: recipient.name + }))); + + msg.setSubject(subject); + + msg.addMessage({ + contentType: 'text/html', + data: message.trim() + }); + + if (headers) { + Object.keys(headers).forEach(key => { + if (headers[key]) msg.setHeader(key, headers[key]); + }); + } + + if (attachments?.length > 0) { + for (const file of attachments) { + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const base64Content = buffer.toString("base64"); + + msg.addAttachment({ + filename: file.name, + contentType: file.type || "application/octet-stream", + data: base64Content + }); + } + } + + const emailContent = msg.asRaw(); + const encodedMessage = Buffer.from(emailContent).toString("base64"); + + return { + raw: encodedMessage, + } + } const normalizeSearch = (folder: string, q: string) => { // Handle special folders if (folder === 'trash') { @@ -449,8 +511,15 @@ export const driver = async (config: IConfig): Promise => { ); return messages; }, - create: async (data: any) => { - const res = await gmail.users.messages.send({ userId: 'me', requestBody: data }); + create: async (data) => { + const { raw } = await parseOutgoing(data) + const res = await gmail.users.messages.send({ + userId: 'me', + requestBody: { + raw, + threadId: data.threadId + } + }); return res.data; }, delete: async (id: string) => { diff --git a/apps/mail/app/api/driver/types.ts b/apps/mail/app/api/driver/types.ts index 23d643691..f3129ad76 100644 --- a/apps/mail/app/api/driver/types.ts +++ b/apps/mail/app/api/driver/types.ts @@ -1,8 +1,8 @@ -import { type InitialThread, type ParsedMessage } from '@/types'; +import { type IOutgoingMessage, type InitialThread, type ParsedMessage } from '@/types'; export interface MailManager { get(id: string): Promise; - create(data: any): Promise; + create(data: IOutgoingMessage): Promise; createDraft(data: any): Promise; getDraft: (id: string) => Promise; listDrafts: (q?: string, maxResults?: number, pageToken?: string) => Promise; diff --git a/apps/mail/components/create/editor.tsx b/apps/mail/components/create/editor.tsx index 7fc92b1ef..3e79fccc1 100644 --- a/apps/mail/components/create/editor.tsx +++ b/apps/mail/components/create/editor.tsx @@ -54,7 +54,6 @@ import { Markdown } from 'tiptap-markdown'; import { useReducer, useRef } from 'react'; import { useState } from 'react'; import React from 'react'; -import SignatureDisplay from './signature-display'; import { TextSelection } from 'prosemirror-state'; export const defaultEditorContent = { @@ -129,9 +128,6 @@ interface MenuBarProps { const MenuBar = ({ onAttachmentsChange, - includeSignature, - onSignatureToggle, - hasSignature = false, }: MenuBarProps) => { const { editor } = useCurrentEditor(); const t = useTranslations(); @@ -208,8 +204,8 @@ const MenuBar = ({ <>
-
-
+
+ {/*
- + */}
@@ -451,56 +447,6 @@ const MenuBar = ({ )} - - {hasSignature && onSignatureToggle && ( - <> - - - - )}
@@ -554,7 +500,6 @@ export default function Editor({ includeSignature, onSignatureToggle, signature, - hasSignature, }: EditorProps) { const [state, dispatch] = useReducer(editorReducer, { openNode: false, @@ -703,7 +648,7 @@ export default function Editor({ }), ]} ref={containerRef} - className="min-h-52 cursor-text relative" + className="cursor-text relative" editorProps={{ handleDOMEvents: { mousedown: (view, event) => { @@ -720,25 +665,17 @@ export default function Editor({ view.dispatch(tr); view.focus(); } - + // Let the default handler also run return false; }, keydown: (view, event) => { - // Handle tab key if (event.key === 'Tab' && !event.shiftKey) { if (onTab && onTab()) { event.preventDefault(); return true; } } - - // Handle Command+Enter (Mac) or Ctrl+Enter (Windows/Linux) - // if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { - // event.preventDefault(); - // handleCommandEnter(); - // return true; - // } return handleCommandNavigation(event); }, focus: () => { @@ -769,48 +706,11 @@ export default function Editor({ slotBefore={ } slotAfter={ <> - {signature && includeSignature && ( -
-
-
- {t('pages.createEmail.signature.title') || 'Signature'} -
- -
-
- -
-
- )} } > diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index f751beb21..013a4bc36 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -76,19 +76,10 @@ const Thread = memo( message, selectMode, demo, - onMouseDown, + onClick, sessionData, isKeyboardFocused, - isInQuickActionMode, - selectedQuickActionIndex, - resetNavigation, - }: ConditionalThreadProps & { - folder?: string; - isKeyboardFocused?: boolean; - isInQuickActionMode?: boolean; - selectedQuickActionIndex?: number; - resetNavigation?: () => void; - }) => { + }: ConditionalThreadProps) => { const [mail] = useMail(); const [searchValue] = useSearchValue(); const t = useTranslations(); @@ -162,7 +153,7 @@ const Thread = memo( }, []); const content = ( -
+
{demo ? (
- + {highlightText(message.sender.name, searchValue.highlight)} {' '} {message.unread && !isMailSelected ? ( @@ -369,7 +360,7 @@ export function MailListDemo({ key={item.id} message={item} selectMode={'single'} - onMouseDown={(message) => () => onSelectMail && onSelectMail(message)} + onClick={(message) => () => onSelectMail && onSelectMail(message)} /> ) : null; })} @@ -567,7 +558,7 @@ export const MailList = memo(({ isCompact }: MailListProps) => { return 'single'; }, [isKeyPressed]); - const handleMailMouseDown = useCallback( + const handleMailClick = useCallback( (message: InitialThread) => () => { handleMouseEnter(message.id); @@ -576,21 +567,12 @@ export const MailList = memo(({ isCompact }: MailListProps) => { // Update local state immediately for optimistic UI setMail((prev) => ({ ...prev, - selected: messageThreadId, replyComposerOpen: false, forwardComposerOpen: false })); // Update URL param without navigation void setThreadId(messageThreadId); - - // Mark as read in background - if (message.unread) { - markAsRead({ ids: [messageThreadId] }).catch((error) => { - console.error('Failed to mark email as read:', error); - toast.error(t('common.mail.failedToMarkAsRead')); - }).then(mutate); - } }, [handleMouseEnter, setThreadId, t, setMail], ); @@ -641,7 +623,7 @@ export const MailList = memo(({ isCompact }: MailListProps) => { {items.map((data, index) => { return ( { }; interface ReplyComposeProps { - emailData: ParsedMessage[]; mode?: 'reply' | 'forward'; } @@ -125,10 +128,16 @@ type FormData = { to: string; }; -export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyComposeProps) { +export default function ReplyCompose({ mode = 'reply' }: ReplyComposeProps) { + const [threadId] = useQueryState('threadId'); + const { data: emailData } = useThread(threadId); const [attachments, setAttachments] = useState([]); const { data: session } = useSession(); const [mail, setMail] = useMail(); + const [toInput, setToInput] = useState(''); + const [toEmails, setToEmails] = useState([]); + const [includeSignature, setIncludeSignature] = useState(true); + const { settings } = useSettings(); // Use global state instead of local state const composerIsOpen = mode === 'reply' ? mail.replyComposerOpen : mail.forwardComposerOpen; @@ -219,35 +228,6 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose } }; - const constructReplyBody = ( - formattedMessage: string, - originalDate: string, - originalSender: { name?: string; email?: string } | undefined, - cleanedToEmail: string, - quotedMessage?: string, - ) => { - return ` -
-
- ${formattedMessage} -
-
-
- On ${originalDate}, ${originalSender?.name ? `${originalSender.name} ` : ''}${originalSender?.email ? `<${cleanedToEmail}>` : ''} wrote: -
-
- ${quotedMessage} -
-
-
- `; - }; - - const [toInput, setToInput] = useState(''); - const [toEmails, setToEmails] = useState([]); - const [includeSignature, setIncludeSignature] = useState(true); - const { settings } = useSettings(); - const isValidEmail = (email: string) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); @@ -285,6 +265,7 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose const handleSendEmail = async (e: React.MouseEvent) => { e.preventDefault(); + if (!emailData) return; setIsSubmitting(true); try { const originalEmail = emailData[0]; @@ -294,61 +275,61 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose throw new Error('Active connection email not found'); } + if (!originalEmail) { + throw new Error('Original email not found'); + } + // Handle subject based on mode const subject = mode === 'forward' - ? `Fwd: ${originalEmail?.subject || ''}` - : originalEmail?.subject?.startsWith('Re:') + ? `Fwd: ${originalEmail.subject || ''}` + : originalEmail.subject?.startsWith('Re:') ? originalEmail.subject : `Re: ${originalEmail?.subject || ''}`; - // For forwarding, use the entered email addresses - const recipients = + const recipients: Sender[] = mode === 'forward' - ? toEmails.join(', ') + ? toEmails.map((email) => ({ email, name: 'User' })) : [ - // Original sender - ...(originalEmail?.sender?.email - ? [cleanEmailAddress(originalEmail.sender.email)] - : []), - // All TO recipients - ...(originalEmail?.to?.map((to) => cleanEmailAddress(to.email)) || []), - // All CC recipients - ...(originalEmail?.cc?.map((cc) => cleanEmailAddress(cc.email)) || []), - ] - .filter(Boolean) - .filter( - (email, index, self) => - self.indexOf(email) === index && email.toLowerCase() !== userEmail, - ) - .join(', '); + { + email: cleanEmailAddress(originalEmail.sender.email), + name: originalEmail.sender.name ? originalEmail.sender.name : '' + } + ] + + + const cc: Sender[] | null = originalEmail.cc ? originalEmail.cc.map((to) => ({ + email: cleanEmailAddress(to.email), + name: to.name ? to.name : '' + })) : null if (!recipients) { throw new Error('No valid recipients found'); } - const messageId = originalEmail?.messageId; - const threadId = originalEmail?.threadId; + const messageId = originalEmail.messageId; + const threadId = originalEmail.threadId; const formattedMessage = form.getValues('messageContent'); - const originalDate = new Date(originalEmail?.receivedOn || '').toLocaleString(); - const quotedMessage = originalEmail?.decodedBody; + const originalDate = new Date(originalEmail.receivedOn || '').toLocaleString(); + const quotedMessage = originalEmail.decodedBody; const replyBody = constructReplyBody( formattedMessage, originalDate, - originalEmail?.sender, + originalEmail.sender, recipients, quotedMessage, ); const inReplyTo = messageId; - const existingRefs = originalEmail?.references?.split(' ') || []; + const existingRefs = originalEmail.references?.split(' ') || []; const references = [...existingRefs, originalEmail?.inReplyTo, cleanEmailAddress(messageId)] .filter(Boolean) .join(' '); await sendEmail({ to: recipients, + cc: cc ?? undefined, subject, message: replyBody, attachments, @@ -357,7 +338,6 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose References: references, 'Thread-Id': threadId ?? '', }, - includeSignature: includeSignature && settings?.signature?.enabled, }); form.reset(); @@ -378,14 +358,6 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose } }; - // Add a useEffect to focus the editor when the composer opens - // Initialize signature toggle from settings - useEffect(() => { - if (settings?.signature) { - setIncludeSignature(settings.signature.includeByDefault); - } - }, [settings]); - useEffect(() => { if (composerIsOpen) { // Give the editor time to render before focusing @@ -455,20 +427,21 @@ export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyCompose const isMessageEmpty = !form.getValues('messageContent') || form.getValues('messageContent') === - JSON.stringify({ - type: 'doc', - content: [ - { - type: 'paragraph', - content: [], - }, - ], - }); + JSON.stringify({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [], + }, + ], + }); // Check if form is valid for submission const isFormValid = !isMessageEmpty || attachments.length > 0; const handleAIButtonClick = async () => { + if (!emailData) return; aiDispatch({ type: 'SET_LOADING', payload: true }); try { // Extract relevant information from the email thread for context @@ -561,6 +534,7 @@ ${email.decodedBody || 'No content'} // Helper function to render the header content based on mode const renderHeaderContent = () => { + if (!emailData) return; if (mode === 'forward') { return (
@@ -599,10 +573,6 @@ ${email.decodedBody || 'No content'} type="email" className="text-md relative left-[3px] min-w-[120px] flex-1 bg-transparent placeholder:text-[#616161] placeholder:opacity-50 focus:outline-none" placeholder={toEmails.length ? '' : t('pages.createEmail.example')} - value={toInput} - onChange={(e) => setToInput(e.target.value)} - onKeyDown={handleEmailInputKeyDown} - onBlur={handleEmailInputBlur} />
@@ -629,7 +599,6 @@ ${email.decodedBody || 'No content'} handleAddEmail(toInput); } else if (e.key === 'Backspace' && !toInput && toEmails.length > 0) { setToEmails((emails) => emails.slice(0, -1)); - form.setValue('to', toEmails.join(', ')); } }; @@ -668,10 +637,9 @@ ${email.decodedBody || 'No content'} className="flex h-12 w-full items-center justify-center gap-2 rounded-md" variant="outline" > - + - {t('common.replyCompose.replyTo')}{' '} - {emailData[emailData.length - 1]?.sender?.name || t('common.replyCompose.thisEmail')} + {t('common.replyCompose.replyTo')} All
@@ -679,7 +647,7 @@ ${email.decodedBody || 'No content'} } return null; } - + if (!emailData) return; return (
- {messages ? ( - - ) : null} + />
@@ -135,7 +132,7 @@ function ThreadActionButton({ } export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisplayProps) { - const { data: emailData, isLoading } = useThread(id ?? null); + const { data: emailData, isLoading, mutate: mutateThread } = useThread(id ?? null); const { mutate: mutateThreads } = useThreads(); const searchParams = useSearchParams(); const [isMuted, setIsMuted] = useState(false); @@ -147,6 +144,25 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp const threadIdParam = searchParams.get('threadId'); const threadId = threadParam ?? threadIdParam ?? ''; + /** + * Mark email as read if it's unread, if there are no unread emails, mark the current thread as read + */ + useEffect(() => { + if (!emailData || !id) return; + const unreadEmails = emailData.filter(e => e.unread); + if (unreadEmails.length === 0) { + markAsRead({ ids: [id] }).catch((error) => { + console.error('Failed to mark email as read:', error); + toast.error(t('common.mail.failedToMarkAsRead')); + }).then(() => Promise.all([mutateThread(), mutateThreads()])) + } else { + const ids = [id, ...unreadEmails.map(e => e.id)] + markAsRead({ ids }).catch((error) => { + console.error('Failed to mark email as read:', error); + toast.error(t('common.mail.failedToMarkAsRead')); + }).then(() => Promise.all([mutateThread(), mutateThreads()])) + } + }, [emailData, id]) const isInArchive = folder === FOLDERS.ARCHIVE; const isInSpam = folder === FOLDERS.SPAM; @@ -407,7 +423,6 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp isFullscreen ? 'mb-2' : '' )}>
diff --git a/apps/mail/lib/utils.ts b/apps/mail/lib/utils.ts index 447f537c8..7a833547d 100644 --- a/apps/mail/lib/utils.ts +++ b/apps/mail/lib/utils.ts @@ -5,6 +5,7 @@ import { twMerge } from 'tailwind-merge'; import { JSONContent } from 'novel'; import LZString from 'lz-string'; import axios from 'axios'; +import { Sender } from '@/types'; export const FOLDERS = { SPAM: 'spam', @@ -360,3 +361,42 @@ export const getEmailLogo = (email: string) => { export const generateConversationId = (): string => { return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; }; + +export const contentToHTML = (content: string) => ` + + + + + + + +${content} +`; + + +export const constructReplyBody = ( + formattedMessage: string, + originalDate: string, + originalSender: Sender | undefined, + otherRecipients: Sender[], + quotedMessage?: string, +) => { + const senderName = originalSender?.name || originalSender?.email || 'Unknown Sender'; + const recipientEmails = otherRecipients.map(r => r.email).join(', '); + + return ` +
+
+ ${formattedMessage} +
+
+
+ On ${originalDate}, ${senderName} ${recipientEmails ? `<${recipientEmails}>` : ''} wrote: +
+
+ ${quotedMessage || ''} +
+
+
+ `; +}; \ No newline at end of file diff --git a/apps/mail/package.json b/apps/mail/package.json index 87ae0641c..544617ca0 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -78,6 +78,7 @@ "jotai": "2.12.1", "lucide-react": "0.474.0", "lz-string": "1.5.0", + "mimetext": "^3.0.27", "motion": "12.4.7", "next": "15.2.3", "next-intl": "3.26.5", diff --git a/apps/mail/types/index.ts b/apps/mail/types/index.ts index cf93cc233..8ff95f407 100644 --- a/apps/mail/types/index.ts +++ b/apps/mail/types/index.ts @@ -42,7 +42,7 @@ export interface ParsedMessage { tags: string[]; sender: Sender; to: Sender[]; - cc: Sender[]; + cc: Sender[] | null; tls: boolean; listUnsubscribe?: string; listUnsubscribePost?: string; @@ -99,8 +99,13 @@ export type ThreadProps = { message: InitialThread; selectMode: MailSelectMode; // TODO: enforce types instead of sprinkling "any" - onMouseDown?: (message: InitialThread) => () => any; + onClick?: (message: InitialThread) => () => void; isCompact?: boolean; + folder?: string; + isKeyboardFocused?: boolean; + isInQuickActionMode?: boolean; + selectedQuickActionIndex?: number; + resetNavigation?: () => void; }; export type ConditionalThreadProps = ThreadProps & @@ -108,3 +113,16 @@ export type ConditionalThreadProps = ThreadProps & | { demo?: true; sessionData?: { userId: string; connectionId: string | null } } | { demo?: false; sessionData: { userId: string; connectionId: string | null } } ); + + + +export interface IOutgoingMessage { + to: Sender[]; + cc?: Sender[]; + bcc?: Sender[]; + subject: string + message: string + attachments: any[] + headers: Record + threadId?: string +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index ee145a83d..bb37130ee 100644 --- a/bun.lock +++ b/bun.lock @@ -89,6 +89,7 @@ "jotai": "2.12.1", "lucide-react": "0.474.0", "lz-string": "1.5.0", + "mimetext": "^3.0.27", "motion": "12.4.7", "next": "15.2.3", "next-intl": "3.26.5", @@ -189,6 +190,8 @@ "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], + "@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.27.0", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew=="], + "@better-auth/utils": ["@better-auth/utils@0.2.3", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], @@ -925,6 +928,8 @@ "core-js": ["core-js@3.41.0", "", {}, "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA=="], + "core-js-pure": ["core-js-pure@3.41.0", "", {}, "sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1355,6 +1360,8 @@ "jotai": ["jotai@2.12.1", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ=="], + "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], + "js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="], "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], @@ -1507,6 +1514,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimetext": ["mimetext@3.0.27", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@babel/runtime-corejs3": "^7.26.0", "js-base64": "^3.7.7", "mime-types": "^2.1.35" } }, "sha512-mUhWAsZD1N/K6dbN4+a5Yq78OPnYQw1ubOSMasBntsLQ2S7KVNlvDEA8dwpr4a7PszWMzeslKahAprtwYMgaBA=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], diff --git a/packages/db/src/user_settings_default.ts b/packages/db/src/user_settings_default.ts index b43b56ed1..fba07c6de 100644 --- a/packages/db/src/user_settings_default.ts +++ b/packages/db/src/user_settings_default.ts @@ -7,11 +7,6 @@ export const defaultUserSettings = { externalImages: true, customPrompt: "", trustedSenders: [], - signature: { - enabled: false, - content: "", - includeByDefault: true, - }, } satisfies UserSettings; export const userSettingsSchema = z.object({ @@ -20,12 +15,7 @@ export const userSettingsSchema = z.object({ dynamicContent: z.boolean(), externalImages: z.boolean(), customPrompt: z.string(), - trustedSenders: z.string().array(), - signature: z.object({ - enabled: z.boolean(), - content: z.string(), - includeByDefault: z.boolean(), - }), + trustedSenders: z.string().array().optional(), }); export type UserSettings = z.infer \ No newline at end of file