From 8feedeada6671074532750563fd8d8e64cae3f19 Mon Sep 17 00:00:00 2001
From: "Hahn Axel (hahn)" <axel.hahn@unibe.ch>
Date: Fri, 8 Nov 2024 11:38:11 +0100
Subject: [PATCH] update viewer

---
 viewer.css |  43 +++++++++-----
 viewer.js  | 107 ++++++++++++++++++++++++++++++-----
 viewer.php | 163 +++++++++++++++++++++++++++++++----------------------
 3 files changed, 216 insertions(+), 97 deletions(-)

diff --git a/viewer.css b/viewer.css
index 1fb1d8d..62c31fc 100644
--- a/viewer.css
+++ b/viewer.css
@@ -17,7 +17,10 @@
     --box-bg: var(--darker-bg);
     --box-strong-color: var(--titlebar-bg);
 
-    --button-bg: #e8e8f0;
+    --button-bg: #f8f8f8;
+    --button-bg-active: #e8e8f0;
+    --button-border-active: #669;
+    --button-border-hover: rgba(0,0,80,0.3);
     --button-color: #667;
     --button-close-bg: #fff;
     --button-close-color: #633;
@@ -41,7 +44,6 @@ body{
     font-size: 1.0m;
     margin: 0;
 }
-
 a{
     color: var(--a-color);
 }
@@ -52,17 +54,20 @@ a.button{
     color: var(--button-color);
     text-decoration: none;
     padding: 0.4em;
-    transition: all 0.2s linear;
+    transition: all 0.1s linear;
+}
+a.button:hover{
+    border-color: var(--button-border-hover);
+    box-shadow: none;
+}
+a.button.active{
+    background: var(--button-bg-active);
+    border-top: 4px solid var(--button-border-active);
 }
 a.button.close{
     background: var(--button-close-bg);
     color: var(--button-close-color);
 }
-a.button:hover{
-    box-shadow: none;
-    border-color: rgba(0,0,80,0.3);
-}
-
 footer{
     background-color: var(--footer-bg);
     position: fixed; bottom: 0;
@@ -71,26 +76,32 @@ footer{
     padding: 1em;
     opacity: 0.6;
 }
-
 h1{
     background: var(--titlebar-bg);
     margin: 0;
     padding-left: 0.5em;
     border-bottom: var(--titlebar-border-bottom);
 }
+h1 small{
+    opacity: 0.5;
+}
 h1 a{
     color: var(--titlebar-color);
     text-decoration: none;
 }
-
-
+iframe{
+    border: 0;
+    width: 100%;
+    height: 70em;
+    overflow: scroll;
+}
 pre{
     background-color: var(--pre-bg);
     overflow: scroll;
     padding: 1em;
     border-radius: 1em;
+    margin: 0 0 1em;
 }
-
 th{
     background-color: var(--table-head-bg);
     padding: 0.5em;
@@ -110,8 +121,6 @@ td a, td span{
     padding: 0.5em;
 }
 
-
-
 #messages{
     margin: 1em;
     margin-bottom: 8em;
@@ -133,7 +142,6 @@ td a, td span{
     background-color: var(--msg-hover-bg);
 }
 
-
 #singlemessage{
     background: var(--simglemail-bg);
     border-top-left-radius: 1em;
@@ -188,3 +196,8 @@ td a, td span{
 .right{
     float: right;
 }
+
+.toolbar{
+    padding: 1.5em 1em 1em 0em;
+    width: 97%;
+}
diff --git a/viewer.js b/viewer.js
index e9389ec..f92df55 100644
--- a/viewer.js
+++ b/viewer.js
@@ -2,7 +2,8 @@
 
 PHP EMAIL CATCHER
 
-search functionality
+- search functionality
+- save/ restore view settings
 
 */
 
@@ -11,44 +12,122 @@ search functionality
 // ----------------------------------------------------------------------
 const tableId = 'messagestable';
 const searchId = 'search';
-const lsVar = 'searchEmailCatcher';
+
+const lsVar_prefix = "emailcatcher_";
+const lsVar_search = lsVar_prefix + "_search";
+const lsVar_header = lsVar_prefix + "_showHeader";
+const lsVar_source = lsVar_prefix + "_showSource";
+
+var bViewHeader = lsGet(lsVar_header, 1);
+var bViewSource = lsGet(lsVar_source, 0);
 
 
 // ----------------------------------------------------------------------
 // functions
 // ----------------------------------------------------------------------
 
+/**
+ * Helper load a variable from local storage
+ * @param {string} key           key to read from localstorage
+ * @param {*}      defaultvalue  default value if "null" or "NaN" was returned
+ * @returns 
+ */
+function lsGet(key, defaultvalue) {
+    return localStorage.getItem(key).replace(/^(null|NaN)$/g, defaultvalue);
+}
+
+// ----------------------------------------------------------------------
+
 /**
  * read search field and hide non matching rows
  * @returns void
  */
-function filterTable(){
-    var sFilter=document.getElementById(searchId).value;
-    localStorage.setItem(lsVar, sFilter);
-    var table=document.getElementById(tableId);
-    if (!table){
+function filterTable() {
+    var sFilter = document.getElementById(searchId).value;
+    localStorage.setItem(lsVar_search, sFilter);
+    var table = document.getElementById(tableId);
+    if (!table) {
         return false;
     }
-    var rows=table.rows;
-    for(var i=1;i<rows.length;i++){
-        if(rows[i].innerText.toLowerCase().indexOf(sFilter.toLowerCase()) == -1){
-            rows[i].style.display='none';
+    var rows = table.rows;
+    for (var i = 1; i < rows.length; i++) {
+        if (rows[i].innerText.toLowerCase().indexOf(sFilter.toLowerCase()) == -1) {
+            rows[i].style.display = 'none';
         }
-        else{
-            rows[i].style.display='table-row';
+        else {
+            rows[i].style.display = 'table-row';
         }
     }
 }
 
+// ----------------------------------------------------------------------
+
+/**
+ * Show / hide message header
+ * @global integer  bViewHeader   flag: view header - 1=yes; 0=no
+ */
+function viewHideHeader() {
+    if (!document.getElementById('msg-header')) {
+        return false;
+    }
+    document.getElementById('msg-header').style.display = bViewHeader ? 'block' : 'none';
+    document.getElementById('btn-header').className = bViewHeader ? 'button active' : 'button';
+    return true;
+}
+
+/**
+ * toggle message headers; it inverst bViewHeader and calls viewHideHeader function
+ * @global integer  bViewHeader   flag: view header - 1=yes; 0=no
+ * @returns true
+ */
+function toggleViewHeader() {
+    bViewHeader = Math.abs(bViewHeader - 1);
+    localStorage.setItem(lsVar_header, bViewHeader);
+    viewHideHeader();
+    return true;
+}
+
+/**
+ * Show message source or html
+ * @global integer  bViewSource   flag: view html or source - 1=show source; 0=show html
+ */
+function viewSource(bSource) {
+    if (!document.getElementById('msg-html')) {
+        return false;
+    }
+    document.getElementById('msg-html').style.display = bSource ? 'none' : 'block';
+    document.getElementById('msg-source').style.display = bSource ? 'block' : 'none';
+
+    document.getElementById('btn-html').className = bSource ? 'button' : 'button active';
+    document.getElementById('btn-source').className = bSource ? 'button active' : 'button';
+
+    if (bSource != bViewSource) {
+        bViewSource = bSource;
+        localStorage.setItem(lsVar_source, bSource);
+    }
+    return true;
+}
 
 // ----------------------------------------------------------------------
 // main
 // ----------------------------------------------------------------------
 
-document.getElementById(searchId).value=''+localStorage.getItem(lsVar);
+// --- search field and filter table
+document.getElementById(searchId).value = '' + lsGet(lsVar_search, '');
 
 document.getElementById(searchId).addEventListener('keyup', filterTable);
 document.getElementById(searchId).addEventListener('keypress', filterTable);
 filterTable();
 
+// --- view settings
+var bViewHeader = Math.round(lsGet(lsVar_header, 1));
+var bViewSource = Math.round(lsGet(lsVar_source, 0));
+
+if (bViewHeader !== 0 && bViewHeader !== 1) {
+    bViewHeader = 1;
+}
+
+viewHideHeader();
+viewSource(bViewSource);
+
 // ----------------------------------------------------------------------
diff --git a/viewer.php b/viewer.php
index f2307fc..d8640a6 100644
--- a/viewer.php
+++ b/viewer.php
@@ -14,15 +14,15 @@
  * 2024-10-08  v0.1  initial version
  * 2024-10-09  v0.2  add links
  * 2024-10-21  v0.3  add tiles on top; add email search
+ * 2024-11-08  v0.4  view html view in preview already
  * =======================================================================
  */
 require_once('classes/emailcatcher.class.php');
 
+$_version = "0.4";
 
-// $sJsonFile='/tmp/mailin.txt.json';
-
-$sOpen=$_GET['open'] ?? '';
-$sShowHtml=$_GET['html'] ?? '';
+$sOpen = $_GET['open'] ?? '';
+$sShowHtml = $_GET['html'] ?? '';
 
 // ----------------------------------------------------------------------
 // FUNCTIONS
@@ -32,36 +32,53 @@ $sShowHtml=$_GET['html'] ?? '';
 
 function showEmail($sId)
 {
-    $sReturn='';
-    $oMail=new emailcatcher();
-    if(!$oMail->setId($sId)){
-        $sReturn.="❌ ERROR: Unable to open non existing email id<br>";
+    $sReturn = '';
+    $oMail = new emailcatcher();
+    if (!$oMail->setId($sId)) {
+        $sReturn .= "❌ ERROR: Unable to open non existing email id<br>";
     } else {
 
-        $bIsHtml=strstr( $oMail->getBody(), '<html>');
-        $sReturn.= '<div id="singlemessage">
+        $bIsHtml = strstr($oMail->getBody(), '<html>');
+        $sToolbar = ''
+            . '<a href="#" id="btn-header" onclick="toggleViewHeader(); return false;" class="button active">📜 Header</a> '
+            . ($bIsHtml
+                ? "<span class=\"right\"><a href=\"?open=$sId&html=1\" class=\"button\">💠 HTML in full screen</a></span>"
+                . '&nbsp;&nbsp;&nbsp;&nbsp;'
+                . "<a href=\"#\" id=\"btn-html\" onclick=\"viewSource(0); return false;\" class=\"button active\">🌐 HTML</a> "
+                . "<a href=\"#\" id=\"btn-source\" onclick=\"viewSource(1); return false;\" class=\"button\">📃 Source</a>"
+                : ''
+            );
+
+        $sReturn .= '<div id="singlemessage">
             <div class="header">
                 <span class="right"><a href="?" class="button close">❌</a>&nbsp;&nbsp;&nbsp;</span>
                 <table>
-                    <tr><td class="small">🕜 DATE</td><td>'.$oMail->getField('date').'</td></tr>
-                    <tr><td class="small">👤 TO</td><td>'.$oMail->getField('to').'</td></tr>
+                    <tr><td class="small">🕜 DATE</td><td>' . $oMail->getField('date') . '</td></tr>
+                    <tr><td class="small">👤 TO</td><td>' . $oMail->getField('to') . '</td></tr>
                 </table>
-                <strong>'.$oMail->getField('subject').'</strong>
+                <strong>' . $oMail->getField('subject') . '</strong>
+                <div class="toolbar">' . $sToolbar . '</div>
             </div>
             <div class="content">
-                📜 Header:<br>
-                <pre>'.$oMail->getHeader().'</pre>
-                <br>🗨️ '
-                .($bIsHtml
-                    ? '<a href="?open='.$sId.'&html=1" class="button">👁️ Show message as HTML</a><br><br>'
-                    : 'Text only:'
-                )
-                .'<pre>'.htmlentities($oMail->getBody()).'</pre>'
-                .'<br>
+                <div id="msg-header">
+                    <pre>' . $oMail->getHeader() . '</pre>
+                </div>
+                '
+            . ($bIsHtml
+                ? '<div id="msg-html">'
+                . '<iframe srcdoc="' . str_replace('"', '&quot;', $oMail->getBody()) . '"></iframe>'
+                . '</div>'
+                . '<div id="msg-source" style="display: none;">'
+                . '<pre>' . htmlentities($oMail->getBody()) . '</pre>'
+                . '</div>'
+                : ''
+                . '<pre>' . htmlentities($oMail->getBody()) . '</pre>'
+            )
+            . '<br>
                 <span class="right"><a href="?" class="button close">❌ Close</a></span><br>
                 <br>'
-            .'</div>'
-        .'</div>'
+            . '</div>'
+            . '</div>'
         ;
     }
     return $sReturn;
@@ -69,85 +86,92 @@ function showEmail($sId)
 
 function showHtmlEmail($sId): void
 {
-    $oMail=new emailcatcher();
-    echo '<button onclick="history.back();return false;">back</button><br>';
-    if(!$oMail->setId($sId)){
+    $oMail = new emailcatcher();
+    echo '
+         <a href="#" onclick="history.back();return false;"
+             style="background: #e8e8f0; border: 2px solid rgba(0,0,0,0.05); border-radius: 0.5em; color: #667; font-size: 100%; text-decoration: none; padding: 0.4em 1em; position: fixed; left: 1em; top: 1em;"
+         >&lt;&lt; back</a>
+     ';
+    if (!$oMail->setId($sId)) {
         echo "❌ ERROR: Unable to open non existing email id<br>";
     } else {
-        echo $oMail->getBody();
+        echo '<div style="border-top: 2px dashed #ddd; margin: 4em auto 3em; padding: 1em; width: 98%;">'
+            . $oMail->getBody()
+            . '</div>';
     }
     die();
 }
+
 // ----------------------------------------------------------------------
 // MAIN
 // ----------------------------------------------------------------------
 
-$oMail=new emailcatcher();
-$aEmails=$oMail->readEmails();
+$oMail = new emailcatcher();
+$aEmails = $oMail->readEmails();
 
-$sOut='';
-$sMessage='';
+$sOut = '';
+$sMessage = '';
 
-if(!count($aEmails)){
-    $sOut='❌ No email was found.<br>';
+if (!count($aEmails)) {
+    $sOut = '❌ No email was found.<br>';
 } else {
 
     // get a single email if id was given.
-    if ($sOpen){
-        if($sShowHtml=="1"){
+    if ($sOpen) {
+        if ($sShowHtml == "1") {
             showHtmlEmail($sOpen);
         }
-        $sMessage=showEmail($sOpen);    
+        $sMessage = showEmail($sOpen);
     }
 
     // show list of emails
-    $sTable='';
-    $sLatest='';
-    foreach($aEmails as $aEmail){
+    $sTable = '';
+    $sLatest = '';
+    foreach ($aEmails as $aEmail) {
 
         // --- age of last email
-        $sId=$aEmail['id'];
-        if(!$sLatest){
-            $iAge=date('U') - date('U', strtotime($aEmail['date']));
-            $sLatest='Less than 1 min ago.';
-            if($iAge>60){
-                $sLatest=round($iAge / 60).' min ago';
+        $sId = $aEmail['id'];
+        if (!$sLatest) {
+            $iAge = date('U') - date('U', strtotime($aEmail['date']));
+            $sLatest = 'Just now';
+            if ($iAge > 60) {
+                $sLatest = round($iAge / 60) . ' min ago';
             }
-            if($iAge>60*60){
-                $sLatest=round($iAge / 60 / 60 ).' h ago';
+            if ($iAge > 60 * 60) {
+                $sLatest = round($iAge / 60 / 60) . ' h ago';
             }
-            if($iAge>60*60*24){
-                $sLatest=round($iAge / 60 / 60 / 24 ).' d ago';
+            if ($iAge > 60 * 60 * 24) {
+                $sLatest = round($iAge / 60 / 60 / 24) . ' d ago';
             }
         }
 
         // --- table with emails
-        $sTable.=($sId!=$sOpen 
+        $sTable .= ($sId != $sOpen
             ? '<tr>
-                <td><a href="?open='.$sId.'">✉️ '.htmlentities($aEmail['subject']).'</a></td>
-                <td><a href="?open='.$sId.'">'.htmlentities($aEmail['to']).'</a></td>
-                <td><a href="?open='.$sId.'">'.$aEmail['date'].'</a></td>
+                <td><a href="?open=' . $sId . '">✉️ ' . htmlentities($aEmail['subject']) . '</a></td>
+                <td><a href="?open=' . $sId . '">' . htmlentities($aEmail['to']) . '</a></td>
+                <td><a href="?open=' . $sId . '">' . $aEmail['date'] . '</a></td>
                 </tr>
                 '
             : '<tr class="active">
-                <td><span>🔶 '. htmlentities($aEmail['subject']).'</span></td>
-                <td><span>'.htmlentities($aEmail['to']).'</span></td>
-                <td><span>'.$aEmail['date'].'</span></td>
+                <td><span>🔶 ' . htmlentities($aEmail['subject']) . '</span></td>
+                <td><span>' . htmlentities($aEmail['to']) . '</span></td>
+                <td><span>' . $aEmail['date'] . '</span></td>
             </tr>'
         );
     }
-    $sOut='<div class="box">Messages<br><strong>'.count($aEmails).'</strong></div>'
-        .'<div class="box">Last<br><strong>'.$sLatest .'</strong></div>'
+    $sOut = '<div class="box">Messages<br><strong>' . count($aEmails) . '</strong></div>'
+        . '<div class="box">Last<br><strong>' . $sLatest . '</strong></div>'
         . '<div><input type="text" id="search" size="30" placeholder="Search..."></div>'
-        . '<br><br>' 
-        ;
-    $sOut.='<table id="messagestable">
+        . '<br><br>'
+    ;
+    $sOut .= '<table id="messagestable">
     <thead>
         <tr><th>Subject</th><th>To</th><th class="date">Date</th></tr>
     </thead>
     <tbgody>'
-    .$sTable
-    .'</tbody></table>' 
+        . $sTable
+        . '</tbody></table>'
     ;
 }
 
@@ -158,14 +182,16 @@ if(!count($aEmails)){
 
 ?><!doctype html>
 <html>
+
 <head>
     <title>Email catcher :: viewer</title>
     <link rel="stylesheet" href="viewer.css">
 
 </head>
+
 <body>
-    
-    <h1><a href="?">🕶️ Email catcher :: viewer</a></h1>
+
+    <h1><a href="?">🕶️ Email viewer <small><?php echo $_version ?></small></a></h1>
 
     <div id="messages"><?php echo $sOut ?></div>
 
@@ -176,7 +202,8 @@ if(!count($aEmails)){
     </footer>
 
     <?php echo $sMessage ?>
-    <script src="viewer.js"></script>
+    <script defer src="viewer.js"></script>
 
 </body>
-</html>
+
+</html>
\ No newline at end of file
-- 
GitLab