diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index db6731e13..811806d12 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -16,13 +16,22 @@ import { Check, Printer, } from '../icons/icons'; +import { + Briefcase, + Star, + StickyNote, + Users, + Lock, + Download, + MoreVertical, + HardDriveDownload, +} from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; -import { Briefcase, Star, StickyNote, Users, Lock, Download, MoreVertical } from 'lucide-react'; import { memo, useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; @@ -52,6 +61,7 @@ import { format, set } from 'date-fns'; import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Badge } from '../ui/badge'; +import JSZip from 'jszip'; // Add formatFileSize utility function const formatFileSize = (size: number) => { @@ -303,6 +313,113 @@ const ActionButton = ({ onClick, icon, text, shortcut }: ActionButtonProps) => { ); }; +const downloadAttachment = (attachment: { body: string; mimeType: string; filename: string }) => { + try { + const byteCharacters = atob(attachment.body); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: attachment.mimeType }); + + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = attachment.filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading attachment:', error); + } +}; + +const handleDownloadAllAttachments = + (subject: string, attachments: { body: string; mimeType: string; filename: string }[]) => () => { + if (!attachments.length) return; + + const zip = new JSZip(); + + console.log('attachments', attachments); + attachments.forEach((attachment) => { + try { + const byteCharacters = atob(attachment.body); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + zip.file(attachment.filename, byteArray, { + binary: true, + date: new Date(), + unixPermissions: 0o644, + }); + } catch (error) { + console.error(`Error adding ${attachment.filename} to zip:`, error); + } + }); + + // Generate and download the zip file + zip + .generateAsync({ + type: 'blob', + compression: 'DEFLATE', + compressionOptions: { + level: 9, + }, + }) + .then((content) => { + const url = window.URL.createObjectURL(content); + const link = document.createElement('a'); + link.href = url; + link.download = `attachments-${subject || 'email'}.zip`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }) + .catch((error) => { + console.error('Error generating zip file:', error); + }); + + console.log('downloaded', subject, attachments); + }; + +const openAttachment = (attachment: { body: string; mimeType: string; filename: string }) => { + try { + const byteCharacters = atob(attachment.body); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: attachment.mimeType }); + const url = window.URL.createObjectURL(blob); + + const width = 800; + const height = 600; + const left = (window.screen.width - width) / 2; + const top = (window.screen.height - height) / 2; + + const popup = window.open( + url, + 'attachment-viewer', + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=no,location=no,menubar=no`, + ); + + if (popup) { + popup.focus(); + // Clean up the URL after a short delay to ensure the browser has time to load it + setTimeout(() => window.URL.revokeObjectURL(url), 1000); + } + } catch (error) { + console.error('Error opening attachment:', error); + } +}; + const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { const [isCollapsed, setIsCollapsed] = useState(false); // const [unsubscribed, setUnsubscribed] = useState(false); @@ -1063,6 +1180,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { { + e.preventDefault(); e.stopPropagation(); printMail(); }} @@ -1070,6 +1188,20 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { Print + { + e.stopPropagation(); + e.preventDefault(); + handleDownloadAllAttachments( + emailData.subject || 'email', + emailData.attachments || [], + )(); + }} + > + + Download All Attachments + @@ -1199,33 +1331,10 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { {emailData?.attachments && emailData?.attachments.length > 0 ? (
{emailData?.attachments.map((attachment, index) => ( -
+
+ + {index < (emailData?.attachments?.length || 0) - 1 && ( +
+ )}
))}
diff --git a/apps/mail/package.json b/apps/mail/package.json index da8026172..578884873 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -66,6 +66,7 @@ "input-otp": "1.4.2", "isbot": "^5.1.28", "jotai": "2.12.1", + "jszip": "3.10.1", "lowlight": "3.3.0", "lucide-react": "0.474.0", "lz-string": "1.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f80c62d21..bb26cb3af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: jotai: specifier: 2.12.1 version: 2.12.1(@types/react@19.0.10)(react@19.0.0) + jszip: + specifier: 3.10.1 + version: 3.10.1 lowlight: specifier: 3.3.0 version: 3.3.0 @@ -4044,6 +4047,9 @@ packages: core-js@3.42.0: resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -5001,6 +5007,9 @@ packages: resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5184,6 +5193,9 @@ packages: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -5295,6 +5307,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + just-camel-case@6.2.0: resolution: {integrity: sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg==} @@ -5328,6 +5343,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -5840,6 +5858,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -6112,6 +6133,9 @@ packages: resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -6446,6 +6470,9 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -6635,6 +6662,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -6792,6 +6822,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -10938,6 +10971,8 @@ snapshots: core-js@3.42.0: {} + core-util-is@1.0.3: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -12093,6 +12128,8 @@ snapshots: ignore@7.0.4: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -12268,6 +12305,8 @@ snapshots: is-what@4.1.16: {} + isarray@1.0.0: {} + isarray@2.0.5: {} isbot@5.1.28: {} @@ -12363,6 +12402,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + just-camel-case@6.2.0: {} jwa@2.0.1: @@ -12402,6 +12448,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} linebreak@1.1.0: @@ -13076,6 +13126,8 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -13265,6 +13317,8 @@ snapshots: proc-log@3.0.0: {} + process-nextick-args@2.0.1: {} + promise-inflight@1.0.1: {} promise-retry@2.0.1: @@ -13738,6 +13792,16 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -13900,8 +13964,7 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.1.2: - optional: true + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -14047,6 +14110,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} sharp@0.33.5: @@ -14286,6 +14351,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0