Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
1a0ad90
draft fixes:
ahmetskilinc Apr 19, 2025
1622cca
some fixes for saving attachments to draft
ahmetskilinc Apr 19, 2025
eea8827
fix for empty draft loading
ahmetskilinc Apr 19, 2025
3a2860e
fix draft list recipient name/address
ahmetskilinc Apr 19, 2025
397090c
also show 'No Recipient' if empty
ahmetskilinc Apr 19, 2025
cc52f73
remove comments
ahmetskilinc Apr 19, 2025
b66393c
Merge branch 'staging' into fix-some-draft-issues
ahmetskilinc Apr 20, 2025
aaf0678
Merge branch 'staging' into fix-some-draft-issues
ahmetskilinc Apr 21, 2025
0214edb
switch to mimetext for draft saving to keep formatting consistent
ahmetskilinc Apr 21, 2025
3253ec3
add message title to draft list
ahmetskilinc Apr 21, 2025
181f606
Merge branch 'staging' into fix-some-draft-issues
ahmetskilinc Apr 21, 2025
281e215
feat: single api for oauth connections
BlankParticle Apr 21, 2025
de0ea3f
Merge branch 'staging' into fix-some-draft-issues
ahmetskilinc Apr 21, 2025
80e1af9
Merge pull request #723 from Mail-0/fix-some-draft-issues
ahmetskilinc Apr 21, 2025
2763b45
fix: add extra error handling
BlankParticle Apr 21, 2025
3ac1712
Merge branch 'staging' into feat/single-point-of-oauth
ahmetskilinc Apr 21, 2025
37e5190
Merge pull request #741 from BlankParticle/feat/single-point-of-oauth
ahmetskilinc Apr 21, 2025
b3415c3
chore: simplify and fix the dev env
BlankParticle Apr 21, 2025
3364807
Merge branch 'staging' into chore/fix-dev-env
MrgSub Apr 21, 2025
d57570c
Merge pull request #742 from BlankParticle/chore/fix-dev-env
MrgSub Apr 21, 2025
1d37659
Ai generate security (#706)
ripgrim Apr 21, 2025
797a8cd
Add a new Vietnamese translation file to support Vietnamese language …
ncdai Apr 21, 2025
57936a1
Update es.json (#710)
danibaldomir Apr 22, 2025
3b338fb
Update app manifest and add new icons for PWA (#739)
humbernieto Apr 22, 2025
9a75453
feat: allow sending from email aliases added through gmail (#743)
atharvadeosthale Apr 22, 2025
acbf4b6
Refactor IP handling in early-access routes
MrgSub Apr 22, 2025
f59fb33
Add unauthorized error handling in sign out function
MrgSub Apr 22, 2025
156c2fa
Redirect from Home Page on Session (#701)
nikitadrokin Apr 22, 2025
d8065ba
Refactor settings handling and golden ticket logic
MrgSub Apr 22, 2025
f892073
Feat: og:image Generation on /compose route (#730)
ripgrim Apr 22, 2025
9d5ff36
Merge branch 'main' into staging
MrgSub Apr 22, 2025
934e336
Update session check to include user id before redirecting
MrgSub Apr 22, 2025
67a393a
Fix unauthorized error handling in multiple actions
MrgSub Apr 22, 2025
f443556
Enable shortcuts settings in navigation
MrgSub Apr 22, 2025
aac4405
Refactor error handling to return unauthorized gracefully
MrgSub Apr 22, 2025
84fce88
Update Hero component with new imports and link adjustments
MrgSub Apr 22, 2025
0ff71d8
Update redirect URL to use hostname from req object
MrgSub Apr 22, 2025
c504834
Fix redirect URL formatting and add log for missing user ID
MrgSub Apr 22, 2025
46e49d4
Fix error handling in API routes for unauthorized requests
MrgSub Apr 22, 2025
89e730a
Refactor throwUnauthorizedGracefully function for readability
MrgSub Apr 22, 2025
69500a8
Fix error handling in driver routes
MrgSub Apr 22, 2025
f8d0d2e
Handle unauthorized gracefully when getting connections
MrgSub Apr 22, 2025
89af662
Refactor mail actions for better error handling
MrgSub Apr 22, 2025
bd6b50d
Refactor deleteActiveConnection function for readability
MrgSub Apr 22, 2025
d1b1b30
fixed (#752)
ahmetskilinc Apr 22, 2025
2aa5007
Refactor error handling in mail actions to return null or specific er…
MrgSub Apr 23, 2025
b086ebc
Update Google auth provider configuration
MrgSub Apr 23, 2025
b80a60c
Delete connection and update hero text
MrgSub Apr 23, 2025
47d4ffc
Refactor error handling to use StandardizedError class
MrgSub Apr 23, 2025
21f52a0
Refactor error handling for Google API driver
MrgSub Apr 23, 2025
56cd464
Add labels to sidebar, labels settings and useLabels hook (#746)
ahmetskilinc Apr 23, 2025
0474e57
bin count of unread messages
Adarsh9977 Apr 18, 2025
bf38622
dixes drafts not saving persistently
ahmetskilinc Apr 24, 2025
ae40c1f
dont show from field is no aliases
ahmetskilinc Apr 24, 2025
56665d0
limited height of attachment dialog
Adarsh9977 Apr 24, 2025
7b8efca
added delete page
ahmetskilinc Apr 24, 2025
e5edcbe
correct way to delete accounts
ahmetskilinc Apr 24, 2025
00bfe7c
- adds new revokeRefreshToken method to Google driver
ahmetskilinc Apr 24, 2025
29d6be2
Merge branch 'main' into staging
MrgSub Apr 25, 2025
c438279
Add posthog-js dependency and implement label filtering in NavMain co…
MrgSub Apr 25, 2025
c7ddfde
Enhance NavItem component to support onClick event handling
MrgSub Apr 25, 2025
96c87ad
fix: add missing dompurify dep (#765)
BlankParticle Apr 25, 2025
37124d6
user can edit enail after selecting (#760)
Adarsh9977 Apr 25, 2025
e9f159d
User can able to delete from bin (#670)
Adarsh9977 Apr 25, 2025
7915165
send draft instead of new message (#767)
ahmetskilinc Apr 25, 2025
2627fee
Add Chinese language support for mail app
MrgSub Apr 25, 2025
4fa171f
Merge branch 'main' into staging
MrgSub Apr 25, 2025
76efa60
Update email addresses in send function
MrgSub Apr 25, 2025
53d41b0
Add import statement for deleteActiveConnection function
MrgSub Apr 25, 2025
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
15 changes: 14 additions & 1 deletion apps/mail/actions/mail.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use server';
import { deleteActiveConnection, FatalErrors, getActiveDriver } from './utils';
import { IGetThreadResponse } from '@/app/api/driver/types';
import { getActiveDriver } from './utils';
import { ParsedMessage } from '@/types';

export const getMail = async ({ id }: { id: string }): Promise<IGetThreadResponse | null> => {
Expand Down Expand Up @@ -113,3 +113,16 @@ export const toggleStar = async ({ ids }: { ids: string[] }) => {
throw error;
}
};

export const deleteThread = async ({ id }: { id: string }) => {
console.log('Deleting thread:', id);
try {
const driver = await getActiveDriver();
await driver.delete(id);
return { success: true };
} catch (error) {
if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection();
console.error('Error deleting thread:', error);
throw error;
}
};
12 changes: 10 additions & 2 deletions apps/mail/actions/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function sendEmail({
headers: additionalHeaders = {},
threadId,
fromEmail,
draftId,
}: {
to: Sender[];
subject: string;
Expand All @@ -24,6 +25,7 @@ export async function sendEmail({
bcc?: Sender[];
threadId?: string;
fromEmail?: string;
draftId?: string;
}) {
if (!to || !subject || !message) {
throw new Error('Missing required fields');
Expand All @@ -43,7 +45,7 @@ export async function sendEmail({
},
});

await driver.create({
const emailData = {
subject,
to,
message,
Expand All @@ -53,7 +55,13 @@ export async function sendEmail({
bcc,
threadId,
fromEmail,
});
};

if (draftId) {
await driver.sendDraft(draftId, emailData);
} else {
await driver.create(emailData);
}

return { success: true };
}
19 changes: 19 additions & 0 deletions apps/mail/app/api/driver/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,25 @@ export const driver = async (config: IConfig): Promise<MailManager> => {
{ threadIds, options },
);
},
sendDraft: async (draftId: string, data: IOutgoingMessage) => {
return withErrorHandler(
'sendDraft',
async () => {
const { raw } = await parseOutgoing(data);
await gmail.users.drafts.send({
userId: 'me',
requestBody: {
id: draftId,
message: {
raw,
id: draftId,
},
},
});
},
{ draftId, data },
);
},
getDraft: async (draftId: string) => {
return withErrorHandler(
'getDraft',
Expand Down
1 change: 1 addition & 0 deletions apps/mail/app/api/driver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface IGetThreadResponse {
export interface MailManager {
get(id: string): Promise<IGetThreadResponse>;
create(data: IOutgoingMessage): Promise<any>;
sendDraft(id: string, data: IOutgoingMessage): Promise<any>;
createDraft(data: any): Promise<any>;
getDraft: (id: string) => Promise<any>;
listDrafts: (q?: string, maxResults?: number, pageToken?: string) => Promise<any>;
Expand Down
25 changes: 24 additions & 1 deletion apps/mail/components/context/thread-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
MailOpen,
} from 'lucide-react';
import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions';
import { markAsRead, markAsUnread, toggleStar } from '@/actions/mail';
import { deleteThread, markAsRead, markAsUnread, toggleStar } from '@/actions/mail';
import { useThread, useThreads } from '@/hooks/use-threads';
import { useSearchValue } from '@/hooks/use-search-value';
import { useParams, useRouter } from 'next/navigation';
Expand Down Expand Up @@ -236,6 +236,22 @@ export function ThreadContextMenu({
disabled: false,
},
];
const handleDelete = () => async () => {
try {
const promise = deleteThread({ id: threadId }).then(() => {
setMail(prev => ({ ...prev, bulkSelected: [] }));
return mutate();
});
toast.promise(promise, {
loading: t('common.actions.deletingMail'),
success: t('common.actions.deletedMail'),
error: t('common.actions.failedToDeleteMail'),
});
} catch (error) {
console.error(`Error deleting ${threadId? 'email' : 'thread'}:`, error);
}
};


const getActions = () => {
if (isSpam) {
Expand Down Expand Up @@ -266,6 +282,13 @@ export function ThreadContextMenu({
action: handleMove(LABELS.TRASH, LABELS.INBOX),
disabled: false,
},
{
id: 'delete-from-bin',
label: t('common.mail.deleteFromBin'),
icon: <Trash className="mr-2.5 h-4 w-4" />,
action: handleDelete(),
disabled: false,
}
];
}

Expand Down
36 changes: 33 additions & 3 deletions apps/mail/components/create/create-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@ export function CreateEmail({
}
};

const handleEditEmail = (type: 'to' | 'cc' | 'bcc', index: number, newEmail: string) => {
// Only validate and edit when Enter is pressed
const trimmedEmail = newEmail.trim();
if (!trimmedEmail) return;

const emailState = type === 'to' ? toEmails : type === 'cc' ? ccEmails : bccEmails;
const setEmailState = type === 'to' ? setToEmails : type === 'cc' ? setCcEmails : setBccEmails;

if (isValidEmail(trimmedEmail)) {
const newEmails = [...emailState];
newEmails[index] = trimmedEmail;
setEmailState(newEmails);
setHasUnsavedChanges(true);
} else {
// Show error for invalid email
toast.error(t('pages.createEmail.invalidEmail'));
}
};

const saveDraft = React.useCallback(async () => {
if (!hasUnsavedChanges) return;
if (!toEmails.length || !subjectInput || !messageContent) return;
Expand Down Expand Up @@ -335,7 +354,7 @@ export function CreateEmail({
// Use the selected from email or the first alias (or default user email)
const fromEmail = selectedFromEmail || (aliases?.[0]?.email ?? userEmail);

await sendEmail({
const emailData = {
to: toEmails.map((email) => ({ email, name: email.split('@')[0] || email })),
cc: showCc
? ccEmails.map((email) => ({ email, name: email.split('@')[0] || email }))
Expand All @@ -347,7 +366,13 @@ export function CreateEmail({
message: messageContent,
attachments: attachments,
fromEmail: fromEmail,
});
};

if (draftId) {
await sendEmail({ ...emailData, draftId });
} else {
await sendEmail(emailData);
}

// Track different email sending scenarios
if (showCc && showBcc) {
Expand Down Expand Up @@ -379,6 +404,8 @@ export function CreateEmail({
setResetEditorKey((prev) => prev + 1);

setHasUnsavedChanges(false);
// Clear the draftId after successful send
setDraftId(null);
} catch (error) {
console.error('Error sending email:', error);
setIsLoading(false);
Expand Down Expand Up @@ -570,6 +597,7 @@ export function CreateEmail({
filteredContacts={[]}
isLoading={isLoading}
onAddEmail={handleAddEmail}
onEditEmail={handleEditEmail}
hasUnsavedChanges={hasUnsavedChanges}
setHasUnsavedChanges={setHasUnsavedChanges}
className="w-24 text-right"
Expand All @@ -585,6 +613,7 @@ export function CreateEmail({
filteredContacts={[]}
isLoading={isLoading}
onAddEmail={handleAddEmail}
onEditEmail={handleEditEmail}
hasUnsavedChanges={hasUnsavedChanges}
setHasUnsavedChanges={setHasUnsavedChanges}
className="w-24 text-right"
Expand All @@ -601,6 +630,7 @@ export function CreateEmail({
filteredContacts={[]}
isLoading={isLoading}
onAddEmail={handleAddEmail}
onEditEmail={handleEditEmail}
hasUnsavedChanges={hasUnsavedChanges}
setHasUnsavedChanges={setHasUnsavedChanges}
className="w-24 text-right"
Expand Down Expand Up @@ -784,7 +814,7 @@ export function CreateEmail({
</p>
</div>
<Separator />
<div className="touch-auto overflow-y-auto max-h-[40vh] overflow-x-hidden overscroll-contain px-1 py-1">
<div className="max-h-[40vh] touch-auto overflow-y-auto overflow-x-hidden overscroll-contain px-1 py-1">
<div className="grid grid-cols-2 gap-2">
{attachments.map((file, index) => (
<div
Expand Down
59 changes: 53 additions & 6 deletions apps/mail/components/create/email-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface EmailInputProps {
filteredContacts: any[];
isLoading: boolean;
onAddEmail: (type: 'to' | 'cc' | 'bcc', email: string) => void;
onEditEmail: (type: 'to' | 'cc' | 'bcc', index: number, email: string) => void;
hasUnsavedChanges: boolean;
setHasUnsavedChanges: (value: boolean) => void;
className?: string;
Expand All @@ -25,13 +26,17 @@ export function EmailInput({
filteredContacts,
isLoading,
onAddEmail,
onEditEmail,
className,
setHasUnsavedChanges,
}: EmailInputProps) {
const t = useTranslations();
const [selectedContactIndex, setSelectedContactIndex] = useState(0);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editValue, setEditValue] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const editInputRef = useRef<HTMLInputElement>(null);

const handleEmailInputChange = (value: string) => {
setInputValue(value);
Expand Down Expand Up @@ -66,11 +71,35 @@ export function EmailInput({
setHasUnsavedChanges(true);
};

const handleChipClick = (index: number, email: string) => {
setEditingIndex(index);
setEditValue(email);
setTimeout(() => {
const input = editInputRef.current;
if (input) {
input.focus();
// Move cursor to the end instead of selecting
const length = input.value.length;
input.setSelectionRange(length, length);
}
}, 0);
};

const handleEditKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (e.key === 'Enter') {
e.preventDefault();
onEditEmail(type, index, editValue);
setEditingIndex(null);
setEditValue('');
} else if (e.key === 'Escape') {
setEditingIndex(null);
setEditValue('');
}
};

return (
<div className="flex items-center">
<div
className={`text-muted-foreground flex-shrink-0 pr-3 text-[1rem] font-[600] opacity-50 ${className}`}
>
<div className={`text-muted-foreground flex-shrink-0 pr-3 text-[1rem] font-[600] opacity-50 ${className}`}>
{type === 'to'
? t('common.mailDisplay.to')
: `${type.charAt(0).toUpperCase()}${type.slice(1)}`}
Expand All @@ -81,9 +110,27 @@ export function EmailInput({
key={index}
className="bg-accent flex items-center gap-1 rounded-md border px-2 text-sm font-medium"
>
<span className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{email}
</span>
{editingIndex === index ? (
<div className="relative flex items-center">
<input
ref={editInputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => handleEditKeyDown(e, index)}
onBlur={() => setEditingIndex(null)}
className="w-[150px] bg-transparent focus:outline-none pr-6"
/>
<span className="absolute right-1 text-xs text-muted-foreground">↵</span>
</div>
) : (
<span
onClick={() => handleChipClick(index, email)}
className="max-w-[150px] cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap"
>
{email}
</span>
)}
<button
tabIndex={-1}
type="button"
Expand Down
2 changes: 2 additions & 0 deletions apps/mail/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const LANGUAGES = {
en: 'English',
ar: 'Arabic',
zh_TW: 'Chinese (Traditional)',
zh_CN: 'Chinese (Simplified)',
ca: 'Catalan',
de: 'German',
es: 'Spanish',
Expand Down
4 changes: 4 additions & 0 deletions apps/mail/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"deletingMail": "Deleting mail...",
"failedToDeleteMail": "Failed to delete mail",
"deletedMail": "Mail deleted",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
Expand Down Expand Up @@ -249,6 +252,7 @@
"mute": "Mute",
"moveToSpam": "Move to Spam",
"moveToInbox": "Move to Inbox",
"deleteFromBin": "Delete from Bin",
"unarchive": "Unarchive",
"archive": "Archive",
"moveToBin": "Move to Bin",
Expand Down
Loading