@@ -129,7 +129,8 @@ class SpamEater {
129129 const modalOverlay = document . getElementById ( 'modalOverlay' ) ;
130130 const modalClose = document . getElementById ( 'modalClose' ) ;
131131 const modalDelete = document . getElementById ( 'modalDelete' ) ;
132-
132+ const modalFullscreen = document . getElementById ( 'modalFullscreen' ) ;
133+
133134 modalOverlay ?. addEventListener ( 'click' , ( e ) => {
134135 if ( e . target === modalOverlay ) this . closeModal ( ) ;
135136 } ) ;
@@ -139,7 +140,8 @@ class SpamEater {
139140 this . deleteEmail ( this . currentEmailData . id , true ) ;
140141 }
141142 } ) ;
142-
143+ modalFullscreen ?. addEventListener ( 'click' , ( ) => this . toggleFullscreen ( ) ) ;
144+
143145 // Headers toggle
144146 const toggleHeaders = document . getElementById ( 'toggleHeaders' ) ;
145147 toggleHeaders ?. addEventListener ( 'click' , ( ) => this . toggleHeaders ( ) ) ;
@@ -280,59 +282,30 @@ class SpamEater {
280282 return sanitized ;
281283 }
282284
283- // Security: Enhanced HTML sanitization to prevent XSS
285+ // Security: HTML sanitization using DOMPurify
286+ // Keeps <style> tags for proper email rendering, displayed in sandboxed iframe
284287 sanitizeHtml ( html ) {
285288 if ( ! html ) return '' ;
286-
287- // Create a temporary element to parse HTML
288- const temp = document . createElement ( 'div' ) ;
289- temp . innerHTML = html ;
290-
291- // Remove all script tags
292- const scripts = temp . querySelectorAll ( 'script' ) ;
293- scripts . forEach ( script => script . remove ( ) ) ;
294-
295- // Remove all elements with event handlers
296- const allElements = temp . querySelectorAll ( '*' ) ;
297- allElements . forEach ( el => {
298- // Remove all event attributes
299- for ( let attr of Array . from ( el . attributes ) ) {
300- if ( attr . name . startsWith ( 'on' ) || attr . name === 'href' && attr . value . startsWith ( 'javascript:' ) ) {
301- el . removeAttribute ( attr . name ) ;
302- }
303- }
304-
305- // Remove javascript: URLs
306- if ( el . href && el . href . startsWith ( 'javascript:' ) ) {
307- el . removeAttribute ( 'href' ) ;
308- }
309- if ( el . src && el . src . startsWith ( 'javascript:' ) ) {
310- el . removeAttribute ( 'src' ) ;
311- }
312-
313- // Remove data: URLs from images (prevent tracking pixels)
314- if ( el . tagName === 'IMG' && el . src && el . src . startsWith ( 'data:' ) ) {
315- el . removeAttribute ( 'src' ) ;
316- el . setAttribute ( 'alt' , '[Image removed for security]' ) ;
317- }
318- } ) ;
319-
320- // Remove dangerous tags
321- const dangerousTags = [ 'iframe' , 'object' , 'embed' , 'link' , 'meta' , 'style' , 'base' , 'form' ] ;
322- dangerousTags . forEach ( tag => {
323- const elements = temp . querySelectorAll ( tag ) ;
324- elements . forEach ( el => el . remove ( ) ) ;
325- } ) ;
326-
327- // Remove SVG with scripts
328- const svgs = temp . querySelectorAll ( 'svg' ) ;
329- svgs . forEach ( svg => {
330- if ( svg . innerHTML . includes ( 'script' ) || svg . innerHTML . includes ( 'onload' ) ) {
331- svg . remove ( ) ;
332- }
289+
290+ // Use DOMPurify for battle-tested XSS prevention
291+ // - ADD_TAGS: ['style'] keeps CSS styling for proper email rendering
292+ // - Content is displayed in sandboxed iframe for extra security
293+ return DOMPurify . sanitize ( html , {
294+ ADD_TAGS : [ 'style' ] , // Keep style tags for email CSS
295+ FORBID_TAGS : [
296+ 'script' , 'iframe' , 'frame' , 'frameset' ,
297+ 'object' , 'embed' , 'applet' , 'form' ,
298+ 'input' , 'button' , 'select' , 'textarea' ,
299+ 'link' , 'meta' , 'base'
300+ ] ,
301+ FORBID_ATTR : [
302+ 'onerror' , 'onload' , 'onclick' , 'onmouseover' ,
303+ 'onfocus' , 'onblur' , 'onchange' , 'onsubmit'
304+ ] ,
305+ ALLOW_DATA_ATTR : false , // No data-* attributes
306+ ALLOW_ARIA_ATTR : true , // Keep accessibility attributes
307+ KEEP_CONTENT : true // Keep text content when removing tags
333308 } ) ;
334-
335- return temp . innerHTML ;
336309 }
337310
338311 async createEmail ( ) {
@@ -667,19 +640,21 @@ class SpamEater {
667640 const div = document . createElement ( 'div' ) ;
668641 div . className = 'email-item' ;
669642 div . setAttribute ( 'data-email-id' , email . id ) ;
670-
643+
671644 const timeAgo = this . formatTimeAgo ( email . receivedAt ) ;
672645 const sender = this . sanitizeText ( email . sender || 'Unknown sender' ) ;
673646 const senderName = email . senderName ? this . sanitizeText ( email . senderName ) : sender ;
674647 const subject = this . sanitizeText ( email . subject || '(No subject)' ) ;
675-
648+
676649 div . innerHTML = `
677650 <div class="email-content-wrapper" data-email-id="${ email . id } ">
678- <div class="email-sender">${ senderName } </div>
679- <div class="email-subject">${ subject } </div>
651+ <div class="email-info">
652+ <div class="email-sender">${ senderName } </div>
653+ <div class="email-subject">${ subject } </div>
654+ </div>
680655 <div class="email-meta">
681- <span class="email-size">${ this . formatBytes ( email . size || 0 ) } </span>
682656 <span class="email-time">${ timeAgo } </span>
657+ <span class="email-size">${ this . formatBytes ( email . size || 0 ) } </span>
683658 </div>
684659 </div>
685660 <button class="delete-btn" title="Delete email" data-email-id="${ email . id } ">
@@ -728,47 +703,152 @@ class SpamEater {
728703 const modalSubject = document . getElementById ( 'modalSubject' ) ;
729704 const modalSender = document . getElementById ( 'modalSender' ) ;
730705 const modalTime = document . getElementById ( 'modalTime' ) ;
731- const modalContent = document . getElementById ( 'modalContent ' ) ;
706+ const emailFrame = document . getElementById ( 'emailFrame ' ) ;
732707 const toggleText = document . getElementById ( 'toggleText' ) ;
733708 const emailHeaders = document . getElementById ( 'emailHeaders' ) ;
734-
709+ const emailModal = document . getElementById ( 'emailModal' ) ;
710+
735711 if ( modalSubject ) modalSubject . textContent = emailData . subject ;
736712 if ( modalSender ) modalSender . textContent = emailData . senderName || emailData . sender ;
737713 if ( modalTime ) modalTime . textContent = emailData . time ;
738- if ( modalContent ) {
739- // Display HTML content if available, otherwise plain text
714+
715+ // Display email content in sandboxed iframe for security
716+ if ( emailFrame ) {
717+ let content ;
740718 if ( emailData . isHtml ) {
741- // Sanitize HTML before displaying
742- modalContent . innerHTML = this . sanitizeHtml ( emailData . content ) ;
719+ // Sanitize HTML with DOMPurify before putting in iframe
720+ const sanitizedHtml = this . sanitizeHtml ( emailData . content ) ;
721+ // Wrap in a basic HTML document with dark theme styling
722+ content = `<!DOCTYPE html>
723+ <html>
724+ <head>
725+ <meta charset="UTF-8">
726+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
727+ <style>
728+ * { box-sizing: border-box; }
729+ html, body {
730+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
731+ font-size: 14px !important;
732+ line-height: 1.6 !important;
733+ color: #e0e0e0 !important;
734+ background: #0a0a0f !important;
735+ margin: 0 !important;
736+ padding: 16px !important;
737+ word-wrap: break-word;
738+ }
739+ /* Force light text on all elements */
740+ div, span, p, td, th, li, label, h1, h2, h3, h4, h5, h6 {
741+ color: #e0e0e0 !important;
742+ }
743+ a { color: #00ff88 !important; }
744+ img { max-width: 100%; height: auto; }
745+ pre, code {
746+ background: #1a1a1f !important;
747+ padding: 2px 6px;
748+ border-radius: 4px;
749+ font-family: 'JetBrains Mono', monospace;
750+ color: #e0e0e0 !important;
751+ }
752+ blockquote {
753+ border-left: 3px solid #00ff88 !important;
754+ margin: 1em 0;
755+ padding-left: 1em;
756+ color: #aaa !important;
757+ }
758+ table { border-collapse: collapse; width: 100%; background: transparent !important; }
759+ td, th { border: 1px solid #333 !important; padding: 8px; color: #e0e0e0 !important; }
760+ </style>
761+ </head>
762+ <body>${ sanitizedHtml } </body>
763+ </html>` ;
743764 } else {
744- // Display plain text (already sanitized)
745- modalContent . textContent = emailData . content ;
765+ // Plain text - wrap in pre tag for proper formatting
766+ const escapedText = emailData . content
767+ . replace ( / & / g, '&' )
768+ . replace ( / < / g, '<' )
769+ . replace ( / > / g, '>' ) ;
770+ content = `<!DOCTYPE html>
771+ <html>
772+ <head>
773+ <meta charset="UTF-8">
774+ <style>
775+ body {
776+ font-family: 'JetBrains Mono', monospace;
777+ font-size: 14px;
778+ line-height: 1.6;
779+ color: #e0e0e0;
780+ background: #0a0a0f;
781+ margin: 0;
782+ padding: 16px;
783+ white-space: pre-wrap;
784+ word-wrap: break-word;
785+ }
786+ </style>
787+ </head>
788+ <body>${ escapedText } </body>
789+ </html>` ;
746790 }
791+
792+ // Use srcdoc for sandboxed content
793+ emailFrame . srcdoc = content ;
747794 }
748-
795+
749796 // Reset headers display
750797 if ( toggleText ) toggleText . textContent = 'Show Headers' ;
751798 if ( emailHeaders ) {
752799 emailHeaders . style . display = 'none' ;
753800 emailHeaders . innerHTML = '' ;
754801 }
755-
802+
803+ // Reset fullscreen state
804+ if ( emailModal ) {
805+ emailModal . classList . remove ( 'modal-fullscreen' ) ;
806+ }
807+
756808 if ( modalOverlay ) {
757809 modalOverlay . style . display = 'flex' ;
758810 document . body . style . overflow = 'hidden' ;
759811 }
760812 }
761-
813+
762814 closeModal ( ) {
763815 const modalOverlay = document . getElementById ( 'modalOverlay' ) ;
816+ const emailFrame = document . getElementById ( 'emailFrame' ) ;
817+ const emailModal = document . getElementById ( 'emailModal' ) ;
818+
764819 if ( modalOverlay ) {
765820 modalOverlay . style . display = 'none' ;
766821 document . body . style . overflow = 'auto' ;
767822 }
768-
823+
824+ // Clear iframe content
825+ if ( emailFrame ) {
826+ emailFrame . srcdoc = '' ;
827+ }
828+
829+ // Reset fullscreen
830+ if ( emailModal ) {
831+ emailModal . classList . remove ( 'modal-fullscreen' ) ;
832+ }
833+
769834 // Clear current email data
770835 this . currentEmailData = null ;
771836 }
837+
838+ toggleFullscreen ( ) {
839+ const emailModal = document . getElementById ( 'emailModal' ) ;
840+ const fullscreenBtn = document . getElementById ( 'modalFullscreen' ) ;
841+
842+ if ( emailModal ) {
843+ emailModal . classList . toggle ( 'modal-fullscreen' ) ;
844+
845+ // Update button icon
846+ if ( fullscreenBtn ) {
847+ fullscreenBtn . textContent = emailModal . classList . contains ( 'modal-fullscreen' ) ? '⛶' : '⛶' ;
848+ fullscreenBtn . title = emailModal . classList . contains ( 'modal-fullscreen' ) ? 'Exit fullscreen' : 'Toggle fullscreen' ;
849+ }
850+ }
851+ }
772852
773853 toggleHeaders ( ) {
774854 const emailHeaders = document . getElementById ( 'emailHeaders' ) ;
0 commit comments