use aliases in email-composer (#1233)

# Add email alias selection to the email composer

## Description

This PR adds the ability for users to select which email alias to send from in the email composer. It displays a dropdown of available email aliases when composing a new email, allowing users to choose their sending identity. The feature is only shown when creating new emails, not when replying to existing messages.

## Type of Change

- [x]  New feature (non-breaking change which adds functionality)
- [x] 🎨 UI/UX improvement

## Areas Affected

- [x] User Interface/Experience

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

- **New Features**
  - Added the ability to select a "From" email address when composing a new email, allowing users to choose from multiple email aliases.
  - Introduced a dropdown menu to display and select available email aliases, with clear indication of the primary alias.
  - Enhanced reply email sender selection to automatically match the sender address with recipients from the original message for more accurate "From" email choice.

- **Style**
  - Updated the subject input area with a new bottom border for improved visual separation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ahmet Kilinc
2025-06-10 21:37:06 +01:00
committed by GitHub
parent 424de39689
commit 766fb69f28
4 changed files with 83 additions and 7 deletions

4
.gitignore vendored
View File

@@ -57,4 +57,6 @@ worker-configuration.d.ts
# devcontainer
.pnpm-store
tsx-0/
tsx-0/
tools.json

View File

@@ -101,9 +101,9 @@ export function CreateEmail({
subject: string;
message: string;
attachments: File[];
fromEmail?: string;
}) => {
// Use the selected from email or the first alias (or default user email)
const fromEmail = aliases?.[0]?.email ?? userEmail;
const fromEmail = data.fromEmail || aliases?.[0]?.email || userEmail;
const zeroSignature = settings?.settings.zeroSignature
? '<p style="color: #666; font-size: 12px;">Sent via <a href="https://0.email/" style="color: #0066cc; text-decoration: none;">Zero</a></p>'
@@ -185,6 +185,7 @@ export function CreateEmail({
<EmailComposer
key={typedDraft?.id || 'composer'}
className="mb-12 rounded-2xl border"
aliases={aliases}
onSendEmail={handleSendEmail}
initialMessage={typedDraft?.content || initialBody}
initialTo={

View File

@@ -7,6 +7,13 @@ import {
X,
Sparkles,
} from '../icons/icons';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { TextEffect } from '@/components/motion-primitives/text-effect';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
@@ -47,6 +54,12 @@ interface EmailComposerProps {
initialSubject?: string;
initialMessage?: string;
initialAttachments?: File[];
replyingTo?: string;
aliases?: {
email: string;
name?: string;
primary?: boolean;
}[];
onSendEmail: (data: {
to: string[];
cc?: string[];
@@ -54,6 +67,7 @@ interface EmailComposerProps {
subject: string;
message: string;
attachments: File[];
fromEmail?: string;
}) => Promise<void>;
onClose?: () => void;
className?: string;
@@ -92,6 +106,8 @@ export function EmailComposer({
className,
autofocus = false,
settingsLoading = false,
replyingTo,
aliases = [],
editorClassName,
}: EmailComposerProps) {
const [showCc, setShowCc] = useState(initialCc.length > 0);
@@ -164,6 +180,7 @@ export function EmailComposer({
subject: initialSubject,
message: initialMessage,
attachments: initialAttachments,
fromEmail: aliases?.find((alias) => alias.primary)?.email || aliases?.[0]?.email || '',
},
});
@@ -247,6 +264,7 @@ export function EmailComposer({
const bccEmails = watch('bcc');
const subjectInput = watch('subject');
const attachments = watch('attachments');
const fromEmail = watch('fromEmail');
const handleAttachment = (files: File[]) => {
if (files && files.length > 0) {
@@ -304,6 +322,7 @@ export function EmailComposer({
subject: values.subject,
message: editor.getHTML(),
attachments: values.attachments || [],
fromEmail: values.fromEmail,
});
setHasUnsavedChanges(false);
editor.commands.clearContent(true);
@@ -908,7 +927,7 @@ export function EmailComposer({
</div>
{/* Subject */}
<div className="flex items-center gap-2 p-3">
<div className="flex items-center gap-2 border-b p-3">
<p className="text-sm font-medium text-[#8C8C8C]">Subject:</p>
<input
className="h-4 w-full bg-transparent text-sm font-normal leading-normal text-black placeholder:text-[#797979] focus:outline-none dark:text-white/90"
@@ -932,6 +951,36 @@ export function EmailComposer({
</button>
</div>
{/* From */}
{aliases.length > 0 && !replyingTo && (
<div className="flex items-center gap-2 border-b p-3">
<p className="text-sm font-medium text-[#8C8C8C]">From:</p>
<Select
value={fromEmail || ''}
onValueChange={(value) => {
setValue('fromEmail', value);
setHasUnsavedChanges(true);
}}
>
<SelectTrigger className="h-6 flex-1 border-0 bg-transparent p-0 text-sm font-normal text-black placeholder:text-[#797979] focus:outline-none focus:ring-0 dark:text-white/90">
<SelectValue placeholder="Select an email address" />
</SelectTrigger>
<SelectContent>
{aliases.map((alias) => (
<SelectItem key={alias.email} value={alias.email}>
<div className="flex flex-row items-center gap-1">
<span className="text-sm">
{alias.name ? `${alias.name} <${alias.email}>` : alias.email}
</span>
{alias.primary && <span className="text-xs text-[#8C8C8C]">Primary</span>}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Message Content */}
<div className="grow self-stretch overflow-y-auto border-t bg-[#FFFFFF] px-3 py-3 outline-white/5 dark:bg-[#202020]">
<div
@@ -939,7 +988,8 @@ export function EmailComposer({
editor.commands.focus();
}}
className={cn(
`max-h-[300px] min-h-[200px] w-full ${editorClassName}`,
`max-h-[300px] min-h-[200px] w-full`,
editorClassName,
aiGeneratedMessage !== null ? 'blur-sm' : '',
)}
>

View File

@@ -112,7 +112,29 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) {
try {
const userEmail = activeConnection.email.toLowerCase();
// Convert email strings to Sender objects
let fromEmail = userEmail;
if (aliases && aliases.length > 0 && replyToMessage) {
const allRecipients = [
...(replyToMessage.to || []),
...(replyToMessage.cc || []),
...(replyToMessage.bcc || []),
];
const matchingAlias = aliases.find((alias) =>
allRecipients.some(
(recipient) => recipient.email.toLowerCase() === alias.email.toLowerCase(),
),
);
if (matchingAlias) {
fromEmail = matchingAlias.email;
} else {
fromEmail =
aliases.find((alias) => alias.primary)?.email || aliases[0]?.email || userEmail;
}
}
const toRecipients: Sender[] = data.to.map((email) => ({
email,
name: email.split('@')[0] || 'User',
@@ -159,7 +181,7 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) {
subject: data.subject,
message: emailBody,
attachments: await serializeFiles(data.attachments),
fromEmail: aliases?.[0]?.email || userEmail,
fromEmail: fromEmail,
headers: {
'In-Reply-To': replyToMessage?.messageId ?? '',
References: [
@@ -246,6 +268,7 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) {
})}
autofocus={shouldFocus}
settingsLoading={settingsLoading}
replyingTo={replyToMessage?.sender.email}
/>
</div>
);