diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 06a49912..ecbc1c87 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -103,6 +103,8 @@ export function EmailComposer({ const timeFormat = useSettingsStore((state) => state.timeFormat); const plainTextMode = useSettingsStore((state) => state.plainTextMode); const autoSelectReplyIdentity = useSettingsStore((state) => state.autoSelectReplyIdentity); + const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled); + const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords); // Initialize with reply/forward data if provided const getInitialTo = () => { @@ -212,6 +214,8 @@ export function EmailComposer({ const [smimePassphrasePrompt, setSmimePassphrasePrompt] = useState<{ keyId: string; resolve: (passphrase: string) => void; reject: () => void } | null>(null); const [smimePassphraseInput, setSmimePassphraseInput] = useState(''); const [smimePassphraseError, setSmimePassphraseError] = useState(''); + const [showAttachmentWarning, setShowAttachmentWarning] = useState(false); + const [attachmentWarningKeyword, setAttachmentWarningKeyword] = useState(''); const saveTemplateModalRef = useFocusTrap({ isActive: showSaveAsTemplate, @@ -225,6 +229,12 @@ export function EmailComposer({ restoreFocus: true, }); + const attachmentWarningRef = useFocusTrap({ + isActive: showAttachmentWarning, + onEscape: () => setShowAttachmentWarning(false), + restoreFocus: true, + }); + const { client } = useAuthStore(); const identities = useIdentityStore((s) => s.identities); const primaryIdentity = identities[0] ?? null; @@ -723,7 +733,7 @@ export function EmailComposer({ return undefined; }; - const handleSend = async () => { + const handleSend = async (skipAttachmentCheck = false) => { const ccAddresses = cc.split(",").map(e => e.trim()).filter(Boolean); const bccAddresses = bcc.split(",").map(e => e.trim()).filter(Boolean); @@ -742,6 +752,21 @@ export function EmailComposer({ return; } + // Attachment reminder check + if (!skipAttachmentCheck && attachmentReminderEnabled) { + const hasAttachments = attachments.some(att => att.blobId && !att.uploading && !att.error); + if (!hasAttachments) { + const bodyText = htmlToPlainText(body); + const searchText = `${subject} ${bodyText}`.toLowerCase(); + const matched = attachmentReminderKeywords.find(kw => searchText.includes(kw.toLowerCase())); + if (matched) { + setAttachmentWarningKeyword(matched); + setShowAttachmentWarning(true); + return; + } + } + } + let finalDraftId = draftId; if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); @@ -1002,7 +1027,7 @@ export function EmailComposer({ {/* Mobile: send button in header */} + + + + + )} + {showCloseDialog && (
+ {/* Attachment Reminder */} + + updateSetting('attachmentReminderEnabled', checked)} + /> + + {attachmentReminderEnabled && ( +
+
+ +

{t('attachment_reminder.keywords_description')}

+
+
+ {attachmentReminderKeywords.map((kw) => ( + + {kw} + + + ))} +
+
{ + e.preventDefault(); + const trimmed = newKeyword.trim().toLowerCase(); + if (trimmed && !attachmentReminderKeywords.includes(trimmed)) { + updateSetting('attachmentReminderKeywords', [...attachmentReminderKeywords, trimmed]); + } + setNewKeyword(''); + }} + > + setNewKeyword(e.target.value)} + placeholder={t('attachment_reminder.add_placeholder')} + className="flex-1 min-w-0 px-2 py-1 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-ring" + /> + +
+
+ )} + {/* Quick Hover Actions */} {isFeatureEnabled('hoverActionsConfigEnabled') && (
diff --git a/locales/de/common.json b/locales/de/common.json index bc6a92e6..0bce3a1a 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -494,7 +494,13 @@ "close_draft_message": "Sie haben ungespeicherte Änderungen. Möchten Sie diese als Entwurf speichern oder verwerfen?", "save_draft": "Entwurf speichern", "drop_files": "Dateien zum Anhängen ablegen", - "show_less": "Weniger anzeigen" + "show_less": "Weniger anzeigen", + "forgot_attachment": { + "title": "Haben Sie den Anhang vergessen?", + "message": "Ihre Nachricht enthält \"{keyword}\", aber es ist keine Datei angehängt. Trotzdem senden?", + "send_anyway": "Trotzdem senden", + "back": "Zurück zur Bearbeitung" + } }, "confirm_dialog": { "confirm": "Bestätigen", @@ -914,6 +920,15 @@ "button": "Als Standard festlegen", "success": "Browser wurde aufgefordert, als Standard festzulegen", "error": "Ihr Browser unterstützt diese Funktion nicht" + }, + "attachment_reminder": { + "label": "Erinnerung an Anhang", + "description": "Warnung anzeigen, wenn die Nachricht Anhänge erwähnt, aber keine angehängt sind", + "keywords_label": "Schlüsselwörter", + "keywords_description": "Wörter oder Phrasen, die die Erinnerung auslösen", + "add_placeholder": "Schlüsselwort hinzufügen...", + "add": "Hinzufügen", + "remove": "Entfernen" } }, "composer": { diff --git a/locales/en/common.json b/locales/en/common.json index 9ecc5d70..8062a10e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -494,7 +494,13 @@ "smime_unlock_title": "Unlock S/MIME Key", "smime_unlock_message": "Enter the passphrase to unlock your S/MIME signing key.", "smime_unlock_button": "Unlock", - "smime_passphrase_placeholder": "Passphrase" + "smime_passphrase_placeholder": "Passphrase", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Confirm", @@ -914,6 +920,15 @@ "button": "Set as Default", "success": "Browser prompted to set as default", "error": "Your browser does not support this feature" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/es/common.json b/locales/es/common.json index c4aed84c..d83a17bc 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -494,7 +494,13 @@ "close_draft_message": "Tiene cambios sin guardar. ¿Desea guardar esto como borrador o descartarlo?", "save_draft": "Guardar borrador", "drop_files": "Suelta archivos para adjuntar", - "show_less": "Mostrar menos" + "show_less": "Mostrar menos", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Confirmar", @@ -914,6 +920,15 @@ "button": "Establecer como predeterminado", "success": "El navegador solicitó establecer como predeterminado", "error": "Su navegador no admite esta función" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/fr/common.json b/locales/fr/common.json index a2f7da9a..855f4976 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -494,7 +494,13 @@ "close_draft_message": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer comme brouillon ou supprimer ?", "save_draft": "Enregistrer le brouillon", "drop_files": "Déposez les fichiers à joindre", - "show_less": "Afficher moins" + "show_less": "Afficher moins", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Confirmer", @@ -914,6 +920,15 @@ "button": "Définir par défaut", "success": "Le navigateur a été invité à définir par défaut", "error": "Votre navigateur ne prend pas en charge cette fonctionnalité" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/it/common.json b/locales/it/common.json index 714b98ea..e48c2294 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -494,7 +494,13 @@ "close_draft_message": "Hai modifiche non salvate. Vuoi salvare come bozza o eliminare?", "save_draft": "Salva bozza", "drop_files": "Trascina i file per allegarli", - "show_less": "Mostra meno" + "show_less": "Mostra meno", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Conferma", @@ -914,6 +920,15 @@ "button": "Imposta come predefinito", "success": "Il browser ha chiesto di impostare come predefinito", "error": "Il tuo browser non supporta questa funzionalità" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/ja/common.json b/locales/ja/common.json index 32ec986c..fc40cc62 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -494,7 +494,13 @@ "close_draft_message": "未保存の変更があります。下書きとして保存しますか、それとも破棄しますか?", "save_draft": "下書きを保存", "drop_files": "ファイルをドロップして添付", - "show_less": "折りたたむ" + "show_less": "折りたたむ", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "確認", @@ -914,6 +920,15 @@ "button": "既定に設定", "success": "ブラウザに既定として設定するよう要求しました", "error": "お使いのブラウザはこの機能をサポートしていません" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/ko/common.json b/locales/ko/common.json index cad0abd5..f64ed071 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -494,7 +494,13 @@ "smime_unlock_title": "S/MIME 키 잠금 해제", "smime_unlock_message": "S/MIME 서명 키의 잠금을 해제하려면 비밀번호를 입력해 주세요.", "smime_unlock_button": "잠금 해제", - "smime_passphrase_placeholder": "비밀번호" + "smime_passphrase_placeholder": "비밀번호", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "확인", @@ -914,6 +920,15 @@ "button": "기본값으로 설정", "success": "브라우저에서 기본 설정 팝업이 뜰 거예요", "error": "이 브라우저에서는 이 기능을 지원하지 않아요" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/lv/common.json b/locales/lv/common.json index ea3a22b7..448cfab7 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -493,7 +493,13 @@ "smime_unlock_title": "Atbloķēt S/MIME atslēgu", "smime_unlock_message": "Ievadiet paroli, lai atbloķētu savu S/MIME parakstīšanas atslēgu.", "smime_unlock_button": "Atbloķēt", - "smime_passphrase_placeholder": "Parole" + "smime_passphrase_placeholder": "Parole", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Apstiprināt", @@ -913,6 +919,15 @@ "button": "Iestatīt kā noklusējumu", "success": "Pārlūkam nosūtīts pieprasījums iestatīt kā noklusējumu", "error": "Jūsu pārlūks neatbalsta šo funkciju" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/nl/common.json b/locales/nl/common.json index e180b8f1..685b19fa 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -494,7 +494,13 @@ "close_draft_message": "U heeft niet-opgeslagen wijzigingen. Wilt u dit als concept opslaan of verwijderen?", "save_draft": "Concept opslaan", "drop_files": "Sleep bestanden om bij te voegen", - "show_less": "Minder tonen" + "show_less": "Minder tonen", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Bevestigen", @@ -914,6 +920,15 @@ "button": "Instellen als standaard", "success": "Browser gevraagd om als standaard in te stellen", "error": "Uw browser ondersteunt deze functie niet" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/pl/common.json b/locales/pl/common.json index 3b64203f..ed1fb15a 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -494,7 +494,13 @@ "smime_unlock_title": "Odblokuj klucz S/MIME", "smime_unlock_message": "Wprowadź hasło, aby odblokować klucz podpisywania S/MIME.", "smime_unlock_button": "Odblokuj", - "smime_passphrase_placeholder": "Hasło" + "smime_passphrase_placeholder": "Hasło", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Potwierdź", @@ -916,6 +922,15 @@ "button": "Ustaw jako domyślny", "success": "Przeglądarka poprosiła o ustawienie jako domyślnego", "error": "Twoja przeglądarka nie obsługuje tej funkcji" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/pt/common.json b/locales/pt/common.json index aa006639..468980a1 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -494,7 +494,13 @@ "close_draft_message": "Você tem alterações não salvas. Deseja salvar como rascunho ou descartar?", "save_draft": "Salvar rascunho", "drop_files": "Solte arquivos para anexar", - "show_less": "Mostrar menos" + "show_less": "Mostrar menos", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Confirmar", @@ -914,6 +920,15 @@ "button": "Definir como padrão", "success": "O navegador solicitou definir como padrão", "error": "Seu navegador não suporta esta funcionalidade" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 34e082e8..fd705832 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -494,7 +494,13 @@ "smime_unlock_title": "Разблокировать ключ S/MIME", "smime_unlock_message": "Введите парольную фразу для разблокировки вашего ключа подписи S/MIME.", "smime_unlock_button": "Разблокировать", - "smime_passphrase_placeholder": "Парольная фраза" + "smime_passphrase_placeholder": "Парольная фраза", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "Подтвердить", @@ -914,6 +920,15 @@ "button": "Установить по умолчанию", "success": "Браузер запрошен для установки по умолчанию", "error": "Ваш браузер не поддерживает эту функцию" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/locales/zh/common.json b/locales/zh/common.json index 2fd33cef..48a47efb 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -494,7 +494,13 @@ "smime_unlock_title": "解锁 S/MIME 密钥", "smime_unlock_message": "输入密码以解锁您的 S/MIME 签名密钥。", "smime_unlock_button": "解锁", - "smime_passphrase_placeholder": "输入密码" + "smime_passphrase_placeholder": "输入密码", + "forgot_attachment": { + "title": "Did you forget an attachment?", + "message": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?", + "send_anyway": "Send anyway", + "back": "Back to editing" + } }, "confirm_dialog": { "confirm": "确认", @@ -914,6 +920,15 @@ "button": "设为默认", "success": "浏览器已提示设置为默认", "error": "您的浏览器不支持此功能" + }, + "attachment_reminder": { + "label": "Attachment Reminder", + "description": "Warn before sending when your message mentions attachments but none are attached", + "keywords_label": "Trigger keywords", + "keywords_description": "Words or phrases that trigger the reminder when found in your message", + "add_placeholder": "Add keyword...", + "add": "Add", + "remove": "Remove" } }, "composer": { diff --git a/stores/settings-store.ts b/stores/settings-store.ts index e500a0a0..870d9fab 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -185,6 +185,10 @@ interface SettingsState { // Keywords (labels/tags) emailKeywords: KeywordDefinition[]; + // Attachment Reminder + attachmentReminderEnabled: boolean; + attachmentReminderKeywords: string[]; + // Sidebar Apps sidebarApps: SidebarApp[]; keepAppsLoaded: boolean; @@ -314,6 +318,37 @@ const DEFAULT_SETTINGS = { // Keywords emailKeywords: DEFAULT_KEYWORDS, + // Attachment Reminder + attachmentReminderEnabled: true, + attachmentReminderKeywords: [ + // English + 'attached', 'attachment', 'attachments', 'see attached', 'find attached', 'please find attached', + // German + 'angehängt', 'anhang', 'anbei', 'im anhang', + // French + 'ci-joint', 'pièce jointe', + // Spanish + 'adjunto', 'adjunta', 'en adjunto', + // Italian + 'allegato', 'in allegato', + // Dutch + 'bijgevoegd', 'bijlage', + // Portuguese + 'em anexo', 'anexo', + // Polish + 'w załączniku', + // Russian + 'во вложении', + // Japanese + '添付', + // Chinese + '附件', + // Korean + '첨부', + // Latvian + 'pielikumā', + ] as string[], + // Sidebar Apps sidebarApps: [] as SidebarApp[], keepAppsLoaded: false, @@ -412,6 +447,8 @@ export const useSettingsStore = create()( senderFavicons: state.senderFavicons, folderIcons: state.folderIcons, emailKeywords: state.emailKeywords, + attachmentReminderEnabled: state.attachmentReminderEnabled, + attachmentReminderKeywords: state.attachmentReminderKeywords, sidebarApps: state.sidebarApps, keepAppsLoaded: state.keepAppsLoaded, debugMode: state.debugMode,