composer styling (#1259)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for forwarding emails, allowing users to send existing messages to new recipients with original content included.
- **Bug Fixes**
  - Improved formatting for quoted and forwarded message sections in email bodies.
- **Chores**
  - Enhanced email composition options to handle more flexible message metadata, such as optional attachments, headers, and recipient fields.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
needle
2025-06-10 05:10:38 +03:00
committed by GitHub
parent 6747ff4276
commit 17828fe541
6 changed files with 70 additions and 19 deletions

View File

@@ -5,7 +5,7 @@ import { useHotkeysContext } from 'react-hotkeys-hook';
import { useTRPC } from '@/providers/query-provider';
import { useMutation } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';
import { constructReplyBody } from '@/lib/utils';
import { constructReplyBody, constructForwardBody } from '@/lib/utils';
import { useThread } from '@/hooks/use-threads';
import { serializeFiles } from '@/lib/schemas';
import { useDraft } from '@/hooks/use-drafts';
@@ -136,20 +136,28 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) {
? '<p style="color: #666; font-size: 12px;">Sent via <a href="https://0.email/" style="color: #0066cc; text-decoration: none;">Zero</a></p>'
: '';
const replyBody = constructReplyBody(
data.message + zeroSignature,
new Date(replyToMessage.receivedOn || '').toLocaleString(),
replyToMessage.sender,
toRecipients,
replyToMessage.decodedBody,
);
const emailBody = mode === 'forward'
? constructForwardBody(
data.message + zeroSignature,
new Date(replyToMessage.receivedOn || '').toLocaleString(),
{ ...replyToMessage.sender, subject: replyToMessage.subject },
toRecipients,
replyToMessage.decodedBody,
)
: constructReplyBody(
data.message + zeroSignature,
new Date(replyToMessage.receivedOn || '').toLocaleString(),
replyToMessage.sender,
toRecipients,
replyToMessage.decodedBody,
);
await sendEmail({
to: toRecipients,
cc: ccRecipients,
bcc: bccRecipients,
subject: data.subject,
message: replyBody,
message: emailBody,
attachments: await serializeFiles(data.attachments),
fromEmail: aliases?.[0]?.email || userEmail,
headers: {
@@ -163,6 +171,8 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) {
'Thread-Id': replyToMessage?.threadId ?? '',
},
threadId: replyToMessage?.threadId,
isForward: mode === 'forward',
originalMessage: replyToMessage.decodedBody,
});
posthog.capture('Reply Email Sent');

View File

@@ -287,8 +287,33 @@ export const constructReplyBody = (
<div style="font-size: 12px;">
On ${originalDate}, ${senderName} ${recipientEmails ? `&lt;${recipientEmails}&gt;` : ''} wrote:
</div>
<div style="">
${quotedMessage || ''}
</div>
</div>
`;
};
export const constructForwardBody = (
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 `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<div style="">
${formattedMessage}
</div>
<div style="margin-top: 20px; border-top: 1px solid #e2e8f0; padding-top: 20px;">
<div style="font-size: 12px; color: #64748b; margin-bottom: 10px;">
---------- Forwarded message ----------<br/>
From: ${senderName} ${originalSender?.email ? `&lt;${originalSender.email}&gt;` : ''}<br/>
Date: ${originalDate}<br/>
Subject: ${originalSender?.subject || 'No Subject'}<br/>
To: ${recipientEmails || 'No Recipients'}
</div>
</div>
</div>

View File

@@ -55,6 +55,7 @@ export interface SidebarData {
export interface Sender {
name?: string;
email: string;
subject?: string;
}
export interface ParsedMessage {
@@ -113,14 +114,16 @@ export type ThreadProps = {
export interface IOutgoingMessage {
to: Sender[];
cc?: Sender[];
bcc?: Sender[];
subject: string;
message: string;
attachments: File[];
headers: Record<string, string>;
attachments?: File[];
headers?: Record<string, string>;
cc?: Sender[];
bcc?: Sender[];
threadId?: string;
fromEmail?: string;
isForward?: boolean;
originalMessage?: string;
}
export interface Note {

View File

@@ -898,6 +898,8 @@ export class GoogleMailManager implements MailManager {
cc,
bcc,
fromEmail,
isForward = false,
originalMessage = null,
}: IOutgoingMessage) {
const msg = createMimeMessage();
@@ -990,10 +992,17 @@ export class GoogleMailManager implements MailManager {
msg.setSubject(subject);
msg.addMessage({
contentType: 'text/html',
data: await sanitizeTipTapHtml(message.trim()),
});
if (originalMessage) {
msg.addMessage({
contentType: 'text/html',
data: `${await sanitizeTipTapHtml(message.trim())}${originalMessage}`,
});
} else {
msg.addMessage({
contentType: 'text/html',
data: await sanitizeTipTapHtml(message.trim()),
});
}
if (headers) {
Object.entries(headers).forEach(([key, value]) => {

View File

@@ -273,6 +273,8 @@ export const mailRouter = router({
threadId: z.string().optional(),
fromEmail: z.string().optional(),
draftId: z.string().optional(),
isForward: z.boolean().optional(),
originalMessage: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {

View File

@@ -136,6 +136,8 @@ export interface IOutgoingMessage {
headers: Record<string, string>;
threadId?: string;
fromEmail?: string;
isForward?: boolean;
originalMessage?: string | null;
}
export interface DeleteAllSpamResponse {
success: boolean;