diff --git a/viewer.css b/viewer.css index 1fb1d8dc15557c6635b902c0eafd46cef5cf7dbe..62c31fc06770face3919220a23e4914c6d597cb8 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 e9389ecd6d784bfb254627e7740da195a050f7f9..f92df556de72818eafbff9504a46438889dd410f 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 f2307fcbf63659fa9196b69151a9880a1582de23..d8640a6b80054d7abc7a5468d7f200717d87c191 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>" + . ' ' + . "<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> </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('"', '"', $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;" + ><< 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