Author: Jim Winstead (jimwins)
Date: 2024-08-23T15:45:39-07:00

Commit: 
https://github.com/php/web-news/commit/9cb47afc5cde13496c736ded39184f01af1625ca
Raw diff: 
https://github.com/php/web-news/commit/9cb47afc5cde13496c736ded39184f01af1625ca.diff

Reformat with phpcbf and a some hand tweaking

Changed paths:
  A  lib/common.php
  A  lib/config.php
  A  lib/group-navbar.php
  M  article.php
  M  common.php
  M  getpart.php
  M  group.php
  M  index.php
  M  lib/Web/News/Nntp.php
  M  lib/fMailbox.php


Diff:

diff --git a/article.php b/article.php
index eb88ad8..578b030 100644
--- a/article.php
+++ b/article.php
@@ -2,217 +2,216 @@
 
 require 'common.php';
 
-/* Prevents the poor mail server from suffering if it receives a message with 
many references */
-/* (References: <xxx> or In-Reply-To: <xxx>) */
-define('REFERENCES_LIMIT', 20);
-
 if (isset($_GET['article'])) {
-       $article = (int)$_GET['article'];
+    $article = (int)$_GET['article'];
 } else {
-       error("No article specified");
+    error("No article specified");
 }
 
 if (isset($_GET['group'])) {
-       $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
+    $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
 } else {
-       $group = false;
+    $group = false;
 }
 
 try {
-       $nntpClient = new \Web\News\Nntp(NNTP_HOST);
-       $message = $nntpClient->readArticle($article, $group);
-
-       if ($message === null) {
-               error('No article found');
-       }
-
-       $mail = fMailbox::parseMessage($message);
-
-       $rawReferences = [];
-       if (!empty($mail['headers']['references'])) {
-               $rawReferences = $mail['headers']['references'];
-       } elseif (!empty($mail['headers']['in-reply-to'])) {
-               $rawReferences = $mail['headers']['in-reply-to'];
-       }
-
-       $references = [];
-       foreach ($rawReferences as $ref) {
-               $matches = [];
-               if (preg_match_all('/\<(.*?)\>/', $ref, $matches)) {
-                       foreach ($matches[0] as $match) {
-                               $references[] = $match;
-                       }
-               }
-       }
-
-       $refsResolved = [];
-
-       $refCount = 0;
-       foreach ($references as $messageId) {
-               if (!$messageId) {
-                       continue;
-               }
-               if ($refCount >= REFERENCES_LIMIT) {
-                       break;
-               }
-               $refsResolved[] = $nntpClient->xpath($messageId);
-               $refCount++;
-       }
+    $nntpClient = new \Web\News\Nntp($NNTP_HOST);
+    $message = $nntpClient->readArticle($article, $group);
+
+    if ($message === null) {
+        error('No article found');
+    }
+
+    $mail = \Flourish\Mailbox::parseMessage($message);
+
+    $rawReferences = [];
+    if (!empty($mail['headers']['references'])) {
+        $rawReferences = $mail['headers']['references'];
+    } elseif (!empty($mail['headers']['in-reply-to'])) {
+        $rawReferences = $mail['headers']['in-reply-to'];
+    }
+
+    $references = [];
+    foreach ($rawReferences as $ref) {
+        $matches = [];
+        if (preg_match_all('/\<(.*?)\>/', $ref, $matches)) {
+            foreach ($matches[0] as $match) {
+                $references[] = $match;
+            }
+        }
+    }
+
+    $refsResolved = [];
+
+    $refCount = 0;
+    foreach ($references as $messageId) {
+        if (!$messageId) {
+            continue;
+        }
+        if ($refCount >= REFERENCES_LIMIT) {
+            break;
+        }
+        $refsResolved[] = $nntpClient->xpath($messageId);
+        $refCount++;
+    }
 } catch (Exception $e) {
-       error($e->getMessage());
+    error($e->getMessage());
 }
 
 head("{$group}: " . format_title($mail['headers']['subject'], 'utf-8'));
 echo '<nav class="secondary-nav">';
 echo ' <ul class="breadcrumbs">';
 echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/">PHP Mailing Lists</a></li>';
-echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/'.htmlspecialchars($group, ENT_QUOTES, 
"UTF-8").'">'.htmlspecialchars($group, ENT_QUOTES, "UTF-8").'</a></li>';
-echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/'.htmlspecialchars($group, ENT_QUOTES, 
"UTF-8").'/'.$article.'">'.format_title($mail['headers']['subject']).'</a></li>';
+echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" href="/' 
.
+    htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '">' .
+    htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '</a></li>';
+echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" href="/' 
.
+    htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '/' . $article . '">' .
+    format_title($mail['headers']['subject']) . '</a></li>';
 echo ' </ul>';
 echo '</nav>';
 echo '<section class="content">';
-start_article($mail, $refsResolved);
+
+echo '<h1>' . format_subject($mail['headers']['subject'], 'utf-8') . "</h1>\n";
+
+echo "  <blockquote>\n";
+echo '   <table class="standard">' . "\n";
+# from
+echo '    <tr class="vcard">' . "\n";
+echo '     <td class="headerlabel">From:</td>' . "\n";
+echo '     <td class="headervalue">' . 
format_author($mail['headers']['from']['raw'], 'utf-8') . "</td>\n";
+# date
+echo '     <td class="headerlabel">Date:</td>' . "\n";
+echo '     <td class="headervalue">' . format_date($mail['headers']['date']) . 
"</td>\n";
+echo "    </tr>\n";
+# subject
+echo '    <tr>' . "\n";
+echo '     <td class="headerlabel">Subject:</td>' . "\n";
+echo '     <td class="headervalue" colspan="3">' . 
format_subject($mail['headers']['subject'], 'utf-8') . "</td>\n";
+echo "    </tr>\n";
+echo "    <tr>\n";
+# references
+if (!empty($refsResolved)) {
+    echo '     <td class="headerlabel">References:</td>' . "\n";
+    echo '     <td class="headervalue" ' . 
(empty($mail['headers']['newsgroups']) ? 'colspan="3"' : null) . '>';
+    foreach ($refsResolved as $k => $ref) {
+        echo "<a href=\"/" . urlencode($ref['group']) . '/' . 
urlencode($ref['articleId']) . "\">" .
+            ($k + 1) . "</a>&nbsp;";
+    }
+    echo "</td>\n";
+}
+# groups
+if (!empty($mail['headers']['newsgroups'])) {
+    echo '     <td class="headerlabel">Groups:</td>' . "\n";
+    echo '     <td class="headervalue" ' . (empty($refsResolved) ? 
'colspan="3"' : null) . '>';
+    $r = explode(",", rtrim($mail['headers']['newsgroups']));
+    foreach ($r as $v) {
+        echo "<a href=\"/" . urlencode($v) . "\">" . htmlspecialchars($v) . 
"</a>&nbsp;";
+    }
+    echo "</td>\n";
+}
+echo "    </tr>\n";
+echo "   </table>\n";
+echo "  </blockquote>\n";
+echo "  <blockquote>\n";
+echo "   <pre>\n";
 
 $lines = preg_split("@(?<=\r\n|\n)@", $mail['text']);
 $insig = 0;
 
 foreach ($lines as $line) {
-       # fix lines that started with a period and got escaped
-       if (substr($line,0,2) == "..") {
-               $line = substr($line,1);
-       }
-
-       # this is some amazingly simplistic code to color quotes/signatures
-       # differently, and turn links into real links. it actually appears
-       # to work fairly well, but could easily be made more sophistimicated.
-       /* NOQUOTES? Why? It creates invalid HTML: http:"x */
-       $line = htmlentities($line,ENT_QUOTES,"utf-8");
-       $line = 
preg_replace("/((mailto|https?|ftp|nntp|news):.+?)(&gt;|\\s|\\)|\\.\\s|$)/","<a 
href=\"\\1\">\\1</a>\\3",$line);
-       if (!$insig && ($line == "-- \r\n" || $line == "--\r\n")) {
-               echo "<span class=\"signature\">";
-               $insig = 1;
-       }
-       if (!$insig && substr($line,0,4) == "&gt;") {
-               echo "<span class=\"quote\">$line</span>";
-       } else {
-               echo $line;
-       }
+    # fix lines that started with a period and got escaped
+    if (substr($line, 0, 2) == "..") {
+        $line = substr($line, 1);
+    }
+
+    # this is some amazingly simplistic code to color quotes/signatures
+    # differently, and turn links into real links. it actually appears
+    # to work fairly well, but could easily be made more sophistimicated.
+    /* NOQUOTES? Why? It creates invalid HTML: http:"x */
+    $line = htmlentities($line, ENT_QUOTES, "utf-8");
+    $line = preg_replace(
+        "/((mailto|https?|ftp|nntp|news):.+?)(&gt;|\\s|\\)|\\.\\s|$)/",
+        "<a href=\"\\1\">\\1</a>\\3",
+        $line
+    );
+    if (!$insig && ($line == "-- \r\n" || $line == "--\r\n")) {
+        echo "<span class=\"signature\">";
+        $insig = 1;
+    }
+    if (!$insig && substr($line, 0, 4) == "&gt;") {
+        echo "<span class=\"quote\">$line</span>";
+    } else {
+        echo $line;
+    }
 }
 
 if ($insig) {
-       echo "</span>";
-       $insig = 0;
+    echo "</span>";
+    $insig = 0;
 }
 
 echo "<br><br>";
 
 if (!empty($mail['attachment'])) {
-       foreach ($mail['attachment'] as $mimecount => $attachment) {
-               $mimetype = $attachment['mimetype'];
-               $name = $attachment['filename'];
-
-               if ($mimetype == 'text/plain') {
-                       echo htmlspecialchars($attachment['data']);
-                       continue;
-               }
-
-               if (!empty($attachment['description'])) {
-                       $description = trim($attachment['description']) . " ";
-               } else {
-                       $description = '';
-               }
-
-               $description .= $name;
-               $link_desc = "[$mimetype]";
-               if (strlen($description)) {
-                       $link_desc .= " " . $description;
-               }
-
-               $dl_link = 
"/getpart.php?group=$group&amp;article=$article&amp;part=$mimecount";
-               $link_desc = htmlspecialchars($link_desc,ENT_QUOTES,'UTF-8');
-
-               /* Attachment filename and mimetype might contain malicious 
characters */
-               printf('Attachment: <a href="%s">%s</a><br />'."\n",
-                       $dl_link,
-                       htmlspecialchars($link_desc)
-               );
-       }
+    foreach ($mail['attachment'] as $mimecount => $attachment) {
+        $mimetype = $attachment['mimetype'];
+        $name = $attachment['filename'];
+
+        if ($mimetype == 'text/plain') {
+            echo htmlspecialchars($attachment['data']);
+            continue;
+        }
+
+        if (!empty($attachment['description'])) {
+            $description = trim($attachment['description']) . " ";
+        } else {
+            $description = '';
+        }
+
+        $description .= $name;
+        $link_desc = "[$mimetype]";
+        if (strlen($description)) {
+            $link_desc .= " " . $description;
+        }
+
+        $dl_link = 
"/getpart.php?group=$group&amp;article=$article&amp;part=$mimecount";
+        $link_desc = htmlspecialchars($link_desc, ENT_QUOTES, 'UTF-8');
+
+        /* Attachment filename and mimetype might contain malicious characters 
*/
+        printf(
+            'Attachment: <a href="%s">%s</a><br />' . "\n",
+            $dl_link,
+            htmlspecialchars($link_desc)
+        );
+    }
 }
 
 echo "   </pre>\n";
 echo "  </blockquote>\n";
 
-function start_article($mail, $refsResolved) {
-
-    echo '<h1>'.format_subject($mail['headers']['subject'], 'utf-8')."</h1>\n";
-
-       echo "  <blockquote>\n";
-       echo '   <table class="standard">' . "\n";
-       # from
-       echo '    <tr class="vcard">' . "\n";
-       echo '     <td class="headerlabel">From:</td>' . "\n";
-       echo '     <td class="headervalue">' . 
format_author($mail['headers']['from']['raw'], 'utf-8')."</td>\n";
-       # date
-       echo '     <td class="headerlabel">Date:</td>' . "\n";
-       echo '     <td class="headervalue">' . 
format_date($mail['headers']['date'])."</td>\n";
-       echo "    </tr>\n";
-       # subject
-       echo '    <tr>' . "\n";
-       echo '     <td class="headerlabel">Subject:</td>' . "\n";
-       echo '     <td class="headervalue" 
colspan="3">'.format_subject($mail['headers']['subject'], 'utf-8')."</td>\n";
-       echo "    </tr>\n";
-       echo "    <tr>\n";
-       # references
-       if (!empty($refsResolved)) {
-               echo '     <td class="headerlabel">References:</td>' . "\n";
-               echo '     <td class="headervalue" 
'.(empty($mail['headers']['newsgroups']) ? 'colspan="3"' : null).'>';
-               foreach ($refsResolved as $k => $ref) {
-                       echo "<a href=\"/". urlencode($ref['group']) . '/' . 
urlencode($ref['articleId']) ."\">".($k + 1)."</a>&nbsp;";
-               }
-               echo "</td>\n";
-       }
-       # groups
-       if (!empty($mail['headers']['newsgroups'])) {
-               echo '     <td class="headerlabel">Groups:</td>' . "\n";
-               echo '     <td class="headervalue" '.(empty($refsResolved) ? 
'colspan="3"' : null).'>';
-               $r = explode(",", rtrim($mail['headers']['newsgroups']));
-               foreach ($r as $v) {
-                       echo "<a 
href=\"/".urlencode($v)."\">".htmlspecialchars($v)."</a>&nbsp;";
-               }
-               echo "</td>\n";
-       }
-       echo "    </tr>\n";
-       echo "   </table>\n";
-       echo "  </blockquote>\n";
-       echo "  <blockquote>\n";
-       echo "   <pre>\n";
-}
-
 // Does not check existence of next, so consider this the super duper fast 
[broken] version
 // Based off navbar() in group.php
-function navbar($group, $current) {
-
-       $group = htmlspecialchars($group, ENT_QUOTES, "UTF-8");
-
-       echo '  <table class="standard">' . "\n";
-       echo '   <tr>' . "\n";
-       echo '    <th class="nav">';
-
-       if ($current > 1) {
-               echo '     <a href="/' , $group , '/' , ($current-1) , 
'"><b>&laquo; <span>previous</span></b></a>';
-       } else {
-               echo '&nbsp;';
-       }
-
-       echo '    </th>' . "\n";
-       echo '    <th class="align-center">' . "$group (#$current)</th>\n";
-       echo '    <th class="nav align-right">';
-       echo '     <a href="/' , $group , '/' , ($current+1) , 
'"><b><span>next</span> &raquo;</b></a>';
-       echo '    </th>' . "\n";
-       echo '   </tr>' . "\n";
-       echo '  </table>' . "\n";
+$group = htmlspecialchars($group, ENT_QUOTES, "UTF-8");
+$current = $article;
+
+echo '  <table class="standard">' . "\n";
+echo '   <tr>' . "\n";
+echo '    <th class="nav">';
+
+if ($current > 1) {
+    echo '     <a href="/' , $group , '/' , ($current - 1) , '"><b>&laquo; 
<span>previous</span></b></a>';
+} else {
+    echo '&nbsp;';
 }
 
-navbar($group, $article);
+echo '    </th>' . "\n";
+echo '    <th class="align-center">' . "$group (#$current)</th>\n";
+echo '    <th class="nav align-right">';
+echo '     <a href="/' , $group , '/' , ($current + 1) , 
'"><b><span>next</span> &raquo;</b></a>';
+echo '    </th>' . "\n";
+echo '   </tr>' . "\n";
+echo '  </table>' . "\n";
 echo '</section>';
+
 foot();
diff --git a/common.php b/common.php
index 28fe391..644fb75 100644
--- a/common.php
+++ b/common.php
@@ -1,172 +1,8 @@
 <?php
 
+# Why autoload when you can just load?
+
+require_once 'lib/common.php';
+require_once 'lib/config.php';
 require_once 'lib/Web/News/Nntp.php';
 require_once 'lib/fMailbox.php';
-
-$NNTP_HOST = 'localhost';
-if (getenv('NNTP_HOST')) {
-       $NNTP_HOST = getenv('NNTP_HOST');
-}
-
-define('NNTP_HOST', $NNTP_HOST);
-
-function error($str) {
-       head("PHP news : error");
-       echo "<section class=\"content\"><blockquote><strong>Error:</strong> 
".to_utf8($str)."</blockquote></section>\n";
-       foot();
-       die();
-}
-
-function head($title="PHP Mailing Lists (PHP News)") {
-       header("Content-type: text/html; charset=utf-8");
-
-?>
-<!doctype html>
-<html lang="en">
- <head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title><?php echo htmlspecialchars($title); ?></title>
-  <link rel="stylesheet" href="/fonts/Fira/fira.css" type="text/css" />
-  <link rel="stylesheet" href="/style.css" type="text/css" />
-  <link rel="shortcut icon" href="/favicon.ico">
- </head>
- <body>
-  <header class="header">
-   <nav class="header-inner">
-    <a href="/" class="header-brand"><img 
src="//php.net/images/logos/php-logo.svg" class="header-brand-img" alt="PHP" 
height="24" width="48"><span class="header-brand-text">lists</span></a><ul 
class="header-menu">
-      <li class="header-menu-item"><a class="header-menu-item-link" 
href="https://php.net/downloads.php";>Downloads</a></li>
-      <li class="header-menu-item"><a class="header-menu-item-link" 
href="https://php.net/docs.php";>Documentation</a></li>
-      <li class="header-menu-item"><a class="header-menu-item-link" 
href="https://php.net/get-involved.php";>Get Involved</a></li>
-      <li class="header-menu-item mod-active"><a class="header-menu-item-link" 
href="https://php.net/support.php";>Help</a></li>
-     </ul>
-     <form class="search-form" action="https://php.net/search.php";>
-      <input class="search-input" value="" name="pattern" placeholder="Search">
-     </form>
-    <div class="menu-icon" 
onclick="document.querySelector('.menu-mobile').classList.toggle('hide')">☰ 
MENU</div>
-     <ul class="menu-mobile hide">
-      <li class="menu-mobile-item"><a class="menu-mobile-item-link" 
href="https://php.net/downloads.php";>Downloads</a></li>
-      <li class="menu-mobile-item"><a class="menu-mobile-item-link" 
href="https://php.net/docs.php";>Documentation</a></li>
-      <li class="menu-mobile-item"><a class="menu-mobile-item-link" 
href="https://php.net/get-involved.php";>Get Involved</a></li>
-      <li class="menu-mobile-item mod-active"><a class="menu-mobile-item-link" 
href="https://php.net/support.php";>Help</a></li>
-     </ul>
-   </nav>
-  </header>
-<?php
-}
-
-function foot() {?>
-
- <footer class="footer">
-    <ul class="footer-nav">
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/copyright.php";>Copyright © 2001-<?php echo date('Y'); ?> 
The PHP Group</a></li>
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/my.php";>My PHP.net</a></li>
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/contact.php";>Contact</a></li>
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/sites.php";>Other PHP.net sites</a></li>
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/mirrors.php";>Mirror sites</a></li>
-     <li class="footer-nav-item"><a class="footer-nav-item-link" 
href="https://php.net/privacy.php";>Privacy policy</a></li>
-    </ul>
- </footer>
- </body>
-</html>
-<?php
-}
-
-function to_utf8($str, $charset = 'iso-8859-1')
-{
-       $n = iconv($charset , 'utf-8', $str);
-       if ($n === false) {
-               return $str;
-       }
-       return $n;
-}
-
-function decode_header($charset,$encoding,$text) {
-       if (strtolower($encoding) == "b") {
-               $text = base64_decode($text);
-       } else {
-               $text = quoted_printable_decode($text);
-       }
-       return to_utf8($text, $charset);
-}
-
-function recode_header($header, $basecharset) {
-       if (strpos($header, "=?") === false) {
-               return to_utf8($header, $basecharset);
-       }
-       return preg_replace_callback(
-               "/=\\?(.+?)\\?([qb])\\?(.+?)(\\?=|$)/i",
-               function ($m) {
-                       return decode_header($m[1], $m[2], $m[3]);
-               },
-               $header
-       );
-}
-
-/* Email spam protection (taken from php-bugs-web) */
-function spam_protect($txt) {
-       $translate = array('@' => ' at ', '.' => ' dot ');
-
-       /* php.net addresses are not protected! */
-       if (preg_match('/^(.+)@php\.net/i', $txt)) {
-               return $txt;
-       } else {
-               return strtr($txt, $translate);
-       }
-}
-
-
-# this turns some common forms of email addresses into mailto: links
-function format_author($a, $charset = 'iso-8859-1') {
-       $a = recode_header($a, $charset);
-       if (preg_match("/^\s*(.+)\s+\\(\"?(.+?)\"?\\)\s*$/",$a,$ar)) {
-               return "<a 
href=\"mailto:".htmlspecialchars(urlencode(spam_protect($ar[1])), ENT_QUOTES, 
"UTF-8")."\" class=\"email fn n\">".str_replace(" ", "&nbsp;", 
htmlspecialchars($ar[2], ENT_QUOTES, "UTF-8"))."</a>";
-       }
-       if (preg_match("/^\s*\"?(.+?)\"?\s*<(.+)>\s*$/",$a,$ar)) {
-               return "<a 
href=\"mailto:".htmlspecialchars(urlencode(spam_protect($ar[2])), ENT_QUOTES, 
"UTF-8")."\" class=\"email fn n\">".str_replace(" ", "&nbsp;", 
htmlspecialchars($ar[1], ENT_QUOTES, "UTF-8"))."</a>";
-       }
-       if (strpos("@",$a) !== false) {
-               $a = spam_protect($a);
-               return "<a href=\"mailto:".htmlspecialchars(urlencode($a), 
ENT_QUOTES, "UTF-8")."\" class=\"email fn n\">".htmlspecialchars($a, 
ENT_QUOTES, "UTF-8")."</a>";
-       }
-       return str_replace(" ", "&nbsp;", htmlspecialchars($a, ENT_QUOTES, 
"UTF-8"));
-}
-
-function format_subject($s, $charset = 'iso-8859-1') {
-       global $article;
-       $s = recode_header($s, $charset);
-
-       if ((($pos = strpos($s, '[PHP')) !== false || ($pos = strpos($s, 
'[PEAR')) !== false)) {
-               if (($end_pos = strpos($s, ']', $pos)) !== false) {
-                       $s = ltrim(substr_replace($s, '', $pos, $end_pos - $pos 
+ 1));
-               }
-       }
-
-       // make this look better on the preview page..
-       if (strlen($s) > 150 && !isset($article)) {
-               $s = substr($s, 0, 150) . "...";
-       } else {
-               $s = wordwrap($s, 150);
-       }
-       return nl2br(htmlspecialchars($s, ENT_QUOTES, "UTF-8"));
-}
-
-
-function format_title($s, $charset = 'iso-8859-1') {
-       global $article;
-       $s = recode_header($s, $charset);
-       $s = preg_replace("/^(Re: *)?\[(PHP|PEAR)(-.*)?\] /i", "\\1", $s);
-       // make this look better on the preview page..
-       if (strlen($s) > 150 && !isset($article)) {
-               $s = substr($s, 0, 150) . "...";
-       } else {
-               $s = wordwrap($s, 150);
-       }
-       return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
-}
-
-function format_date($d) {
-       $d = strtotime($d);
-       $d = gmdate('r', $d);
-       return str_replace(" ", "&nbsp;", $d);
-}
diff --git a/getpart.php b/getpart.php
index 986d04e..c14b616 100644
--- a/getpart.php
+++ b/getpart.php
@@ -3,57 +3,57 @@
 require 'common.php';
 
 if (isset($_GET['group'])) {
-       $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
+    $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
 } else {
-       $group = false;
+    $group = false;
 }
 
 if (isset($_GET['article'])) {
-       $article = (int)$_GET['article'];
+    $article = (int)$_GET['article'];
 } else {
-       error("No article specified");
+    error("No article specified");
 }
 
 if (isset($_GET['part'])) {
-       $part = $_GET['part'];
+    $part = $_GET['part'];
 } else {
-       error("No part specified");
+    error("No part specified");
 }
 
 try {
-       $nntpClient = new \Web\News\Nntp(NNTP_HOST);
-       $message = $nntpClient->readArticle($article, $group);
+    $nntpClient = new \Web\News\Nntp($NNTP_HOST);
+    $message = $nntpClient->readArticle($article, $group);
 
-       if ($message === null) {
-               error('No article found');
-       }
+    if ($message === null) {
+        error('No article found');
+    }
 
-       $mail = fMailbox::parseMessage($message);
+    $mail = \Flourish\Mailbox::parseMessage($message);
 } catch (Exception $e) {
-       error($e->getMessage());
+    error($e->getMessage());
 }
 
 if (!empty($mail['attachment'][$part])) {
-       $attachment = $mail['attachment'][$part];
+    $attachment = $mail['attachment'][$part];
 
-       /* Do not rely on user-provided content-deposition header, generate own 
one to */
-       /* make the content downloadable, do NOT use inline, we can't trust the 
attachment*/
-       /* Downside of this approach: images should be downloaded before use */
-       /* this is safer though, and prevents doing evil things on php.net 
domain */
-       $contentdisposition = 'attachment';
+    /* Do not rely on user-provided content-deposition header, generate own 
one to */
+    /* make the content downloadable, do NOT use inline, we can't trust the 
attachment*/
+    /* Downside of this approach: images should be downloaded before use */
+    /* this is safer though, and prevents doing evil things on php.net domain 
*/
+    $contentdisposition = 'attachment';
 
-       if (!empty($attachment['filename'])) {
-               $contentdisposition .= '; filename="' . $attachment['filename'] 
. '"';
-       }
+    if (!empty($attachment['filename'])) {
+        $contentdisposition .= '; filename="' . $attachment['filename'] . '"';
+    }
 
-       header('Content-Type: ' . $attachment['mimetype']);
-       header('Content-Disposition: ' . $contentdisposition);
+    header('Content-Type: ' . $attachment['mimetype']);
+    header('Content-Disposition: ' . $contentdisposition);
 
-       if (isset($attachment['description'])) {
-               header('Content-Description: ' . $attachment['description']);
-       }
+    if (isset($attachment['description'])) {
+        header('Content-Description: ' . $attachment['description']);
+    }
 
-       echo $attachment['data'];
+    echo $attachment['data'];
 } else {
-       error('Part not found');
+    error('Part not found');
 }
diff --git a/group.php b/group.php
index fb9a380..91f101e 100644
--- a/group.php
+++ b/group.php
@@ -1,81 +1,85 @@
 <?php
 
 require 'common.php';
+require 'lib/group-navbar.php';
 
 if (isset($_GET['group'])) {
-       $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
+    $group = preg_replace('@[^A-Za-z0-9.-]@', '', $_GET['group']);
 } else {
-       error("Missing group");
+    error("Missing group");
 }
 
 if (isset($_GET['format'])) {
-       $format = $_GET['format'];
+    $format = $_GET['format'];
 } else {
-       // assume html
-       $format = 'html';
+    // assume html
+    $format = 'html';
 }
 
 if (isset($_GET['i'])) {
-       $i = (int)$_GET['i'];
+    $i = (int)$_GET['i'];
 } else {
-       $i = 0;
+    $i = 0;
 }
 
 try {
-       $nntpClient = new \Web\News\Nntp(NNTP_HOST);
-       $overview = $nntpClient->getArticlesOverview($group, $i);
+    $nntpClient = new \Web\News\Nntp($NNTP_HOST);
+    $overview = $nntpClient->getArticlesOverview($group, $i);
 } catch (Exception $e) {
-       error($e->getMessage());
+    error($e->getMessage());
 }
 
 $host = htmlspecialchars($_SERVER['HTTP_HOST'], ENT_QUOTES, "UTF-8");
-switch($format) {
-       case 'rss':
-       header("Content-type: text/xml");
-    echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";?>
+switch ($format) {
+    case 'rss':
+        header("Content-type: text/xml");
+        echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";?>
 <rss version="2.0">
  <channel> 
   <title><?php echo $host; ?>: <?php echo $group?></title>
   <link>http://<?php echo $host; ?>/group.php?group=<?php echo $group?></link>
   <description></description>
-<?php  break;
-case 'rdf':
-header("Content-type: text/xml");
-echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
-?>
+        <?php
+        break;
+    case 'rdf':
+        header("Content-type: text/xml");
+        echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
+        ?>
 <rdf:RDF
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
         xmlns="http://my.netscape.com/rdf/simple/0.9/";>
  <channel>
   <title><?php echo $host; ?>: <?php echo $group?></title>
   <link>http://<?php echo $host; ?>/group.php?group=<?php echo $group?></link>
-  <description><?php echo $group?> Newsgroup at <?php echo NNTP_HOST; 
?></description>
+  <description><?php echo $group?> Newsgroup at <?php echo $NNTP_HOST; 
?></description>
   <language>en-US</language>
  </channel>
-<?php
-break;
-case 'html':
-default:
-head($group.' mailing list');
-echo '<nav class="secondary-nav">';
-echo ' <ul class="breadcrumbs">';
-echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/">PHP Mailing Lists</a></li>';
-echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/'.htmlspecialchars($group, ENT_QUOTES, 
"UTF-8").'">'.htmlspecialchars($group, ENT_QUOTES, "UTF-8").'</a></li>';
-echo ' </ul>';
-echo '</nav>';
-echo '<section class="content">';
-echo '<h1>'.htmlspecialchars($group, ENT_QUOTES, "UTF-8").'</h1>';
-navbar($group, $overview['group']['low'], $overview['group']['high'], 
$overview['group']['start']);
-echo ' <div class="responsive-table">' . "\n";
-echo '  <table class="standard">' . "\n";
-echo '   <tr>' . "\n";
-echo '    <th>#</th>' . "\n";
-echo '    <th>subject</th>' . "\n";
-echo '    <th>author</th>' . "\n";
-echo '    <th>date</th>' . "\n";
-echo '    <th>lines</th>' . "\n";
-echo '   </tr>' . "\n";
-break;
+        <?php
+        break;
+    case 'html':
+    default:
+        head($group . ' mailing list');
+        echo '<nav class="secondary-nav">';
+        echo ' <ul class="breadcrumbs">';
+        echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/">PHP Mailing Lists</a></li>';
+        echo '  <li class="breadcrumbs-item"><a class="breadcrumbs-item-link" 
href="/',
+            htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '">',
+            htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '</a></li>';
+        echo ' </ul>';
+        echo '</nav>';
+        echo '<section class="content">';
+        echo '<h1>' . htmlspecialchars($group, ENT_QUOTES, "UTF-8") . '</h1>';
+        navbar($group, $overview['group']['low'], $overview['group']['high'], 
$overview['group']['start']);
+        echo ' <div class="responsive-table">' . "\n";
+        echo '  <table class="standard">' . "\n";
+        echo '   <tr>' . "\n";
+        echo '    <th>#</th>' . "\n";
+        echo '    <th>subject</th>' . "\n";
+        echo '    <th>author</th>' . "\n";
+        echo '    <th>date</th>' . "\n";
+        echo '    <th>lines</th>' . "\n";
+        echo '   </tr>' . "\n";
+        break;
 }
 
 # list of articles
@@ -83,79 +87,57 @@
 $charset = "utf-8";
 
 foreach ($overview['articles'] as $articleNumber => $details) {
-       /*  $date = date("H:i:s M/d/y", strtotime($odate)); */
-       $date822 = date("r", strtotime($details['date']));
+    /*  $date = date("H:i:s M/d/y", strtotime($odate)); */
+    $date822 = date("r", strtotime($details['date']));
 
-       switch($format) {
-               case 'rss':
-               echo "  <item>\n";
-               echo "   <link>http://$host/$group/$articleNumber</link>\n";
-               echo "   <title>", format_subject($details['subject'], 
$charset), "</title>\n";
-               echo "   <description>", 
htmlspecialchars(format_author($details['author'], $charset), ENT_QUOTES, 
"UTF-8"), "</description>\n";
-               echo "   <pubDate>$date822</pubDate>\n";
-               echo "  </item>\n";
-               break;
-               case 'rdf':
-               echo " <item>\n";
-               echo "  <title>", format_subject($details['subject'], 
$charset), "</title>\n";
-               echo "  <link>http://$host/$group/$articleNumber</link>\n";
-               echo "  <description>", 
htmlspecialchars(format_author($details['author'], $charset), ENT_QUOTES, 
"UTF-8"), "</description>\n";
-               echo "  <pubDate>$date822</pubDate>\n";
-               echo " </item>\n";
-               break;
-               case 'html':
-               default:
-               echo "   <tr>\n";
-               echo "    <td><a 
href=\"/$group/$articleNumber\">$articleNumber</a></td>\n";
-               echo "    <td><a href=\"/$group/$articleNumber\">";
-               echo format_subject($details['subject'], $charset);
-               echo "</a></td>\n";
-               echo "    <td 
class=\"vcard\">".format_author($details['author'], $charset)."</td>\n";
-               echo "    <td class=\"align-center\"><span class='monospace 
mod-small'>" . format_date($details['date']) . "</span></td>\n";
-               echo "    <td class=\"align-right\">{$details['lines']}</td>\n";
-               echo "   </tr>\n";
-       }
+    switch ($format) {
+        case 'rss':
+            echo "  <item>\n";
+            echo "   <link>http://$host/$group/$articleNumber</link>\n";
+            echo "   <title>", format_subject($details['subject'], $charset), 
"</title>\n";
+            echo "   <description>",
+                htmlspecialchars(format_author($details['author'], $charset), 
ENT_QUOTES, "UTF-8"),
+                "</description>\n";
+            echo "   <pubDate>$date822</pubDate>\n";
+            echo "  </item>\n";
+            break;
+        case 'rdf':
+            echo " <item>\n";
+            echo "  <title>", format_subject($details['subject'], $charset), 
"</title>\n";
+            echo "  <link>http://$host/$group/$articleNumber</link>\n";
+            echo "  <description>",
+                htmlspecialchars(format_author($details['author'], $charset), 
ENT_QUOTES, "UTF-8"),
+                "</description>\n";
+            echo "  <pubDate>$date822</pubDate>\n";
+            echo " </item>\n";
+            break;
+        case 'html':
+        default:
+            echo "   <tr>\n";
+            echo "    <td><a 
href=\"/$group/$articleNumber\">$articleNumber</a></td>\n";
+            echo "    <td><a href=\"/$group/$articleNumber\">";
+            echo format_subject($details['subject'], $charset);
+            echo "</a></td>\n";
+            echo "    <td class=\"vcard\">" . 
format_author($details['author'], $charset) . "</td>\n";
+            echo "    <td class=\"align-center\"><span class='monospace 
mod-small'>" .
+                format_date($details['date']) . "</span></td>\n";
+            echo "    <td class=\"align-right\">{$details['lines']}</td>\n";
+            echo "   </tr>\n";
+    }
 }
 
 switch ($format) {
-       case 'rss':
-       echo " </channel>\n</rss>\n";
-       break;
-       case 'rdf':
-       echo "</rdf:RDF>\n";
-       break;
-       case 'html':
-       default:
-       echo "  </table>\n";
-       echo " </div>\n";
-       navbar($group, $overview['group']['low'], $overview['group']['high'], 
$overview['group']['start']);
-       echo "</section>";
-       foot();
-}
-
-function navbar($g, $f, $l, $i) {
-       echo '  <table class="standard">' . "\n";
-       echo '   <tr>' . "\n";
-       echo '    <th class="nav">';
-       if ($i > $f) {
-               $p = max($i-20,$f);
-               echo "<a href=\"/" . htmlspecialchars($g, ENT_QUOTES, "UTF-8") 
. "/start/$p\"><b>&laquo; <span>previous</span></b></a>";
-       } else {
-               echo "&nbsp;";
-       }
-       echo '</th>' . "\n";
-       $j = min($i + 20, $l);
-       $c = $l - $f + 1;
-       echo '    <th class="align-center">'.htmlspecialchars($g, ENT_QUOTES, 
"UTF-8")." ($i-$j of $c)</th>\n";
-       echo '    <th class="nav align-right">';
-       if ($i+20 <= $l) {
-               $n = min($i + 20, $l - 19);
-               echo "<a href=\"/" . htmlspecialchars($g, ENT_QUOTES, "UTF-8") 
. "/start/$n\"><b><span>next</span> &raquo;</b></a>";
-       }
-       else {
-               echo "&nbsp;";
-       }
-       echo '</th>' . "\n";
-       echo '   </tr>' . "\n";
-       echo '  </table>' . "\n";
+    case 'rss':
+        echo " </channel>\n</rss>\n";
+        break;
+    case 'rdf':
+        echo "</rdf:RDF>\n";
+        break;
+    case 'html':
+    default:
+        echo "  </table>\n";
+        echo " </div>\n";
+        navbar($group, $overview['group']['low'], $overview['group']['high'], 
$overview['group']['start']);
+        echo "</section>";
+        foot();
 }
diff --git a/index.php b/index.php
index 021ce80..64a9541 100644
--- a/index.php
+++ b/index.php
@@ -3,15 +3,15 @@
 require 'common.php';
 
 try {
-       $nntpClient = new \Web\News\Nntp(NNTP_HOST);
-       $groups = $nntpClient->listGroups();
-       /* Reorder so it's moderated, active, and inactive */
-       $order = [ 'm' => 1, 'y' => 2, 'n' => 3 ];
-       uasort($groups, function ($a, $b) use ($order) {
-               return $order[$a['status']] <=> $order[$b['status']];
-       });
+    $nntpClient = new \Web\News\Nntp($NNTP_HOST);
+    $groups = $nntpClient->listGroups();
+    /* Reorder so it's moderated, active, and inactive */
+    $order = [ 'm' => 1, 'y' => 2, 'n' => 3 ];
+    uasort($groups, function ($a, $b) use ($order) {
+        return $order[$a['status']] <=> $order[$b['status']];
+    });
 } catch (Exception $e) {
-       error($e->getMessage());
+    error($e->getMessage());
 }
 
 head();
@@ -48,26 +48,26 @@
 <?php
 $last_status = 'm';
 foreach ($groups as $group => $details) {
-       if ($details['status'] != $last_status) {
-               $last_status = $details['status'];
-               echo '<tr><th colspan="4">',
-                       $last_status == 'y' ? 'Discussion Lists' : 'Inactive 
Lists',
-                       "</th></tr>\n";
-       }
-       echo "       <tr>\n";
-       echo "        <td><a class=\"active{$details['status']}\" 
href=\"/$group\">$group</a></td>\n";
-       echo "        <td class=\"align-right\">", 
$details['high']-$details['low']+1, "</td>\n";
-       echo "        <td>";
-       if ($details['status'] != 'n') {
-               echo "<a 
href=\"group.php?group=$group&amp;format=rss\">rss</a>";
-       }
-       echo "</td>\n";
-       echo "        <td>";
-       if ($details['status'] != 'n') {
-               echo "<a 
href=\"group.php?group=$group&amp;format=rdf\">rdf</a>";
-       }
-       echo "</td>\n";
-       echo "       </tr>\n";
+    if ($details['status'] != $last_status) {
+        $last_status = $details['status'];
+        echo '<tr><th colspan="4">',
+            $last_status == 'y' ? 'Discussion Lists' : 'Inactive Lists',
+            "</th></tr>\n";
+    }
+    echo "       <tr>\n";
+    echo "        <td><a class=\"active{$details['status']}\" 
href=\"/$group\">$group</a></td>\n";
+    echo "        <td class=\"align-right\">", $details['high'] - 
$details['low'] + 1, "</td>\n";
+    echo "        <td>";
+    if ($details['status'] != 'n') {
+        echo "<a href=\"group.php?group=$group&amp;format=rss\">rss</a>";
+    }
+    echo "</td>\n";
+    echo "        <td>";
+    if ($details['status'] != 'n') {
+        echo "<a href=\"group.php?group=$group&amp;format=rdf\">rdf</a>";
+    }
+    echo "</td>\n";
+    echo "       </tr>\n";
 }
 ?>
   </table>
diff --git a/lib/Web/News/Nntp.php b/lib/Web/News/Nntp.php
index 837c022..10d7c55 100644
--- a/lib/Web/News/Nntp.php
+++ b/lib/Web/News/Nntp.php
@@ -1,4 +1,5 @@
 <?php
+
 namespace Web\News;
 
 /**
@@ -6,229 +7,229 @@
  */
 class Nntp
 {
-       /**
-        * @var resource
-        */
-       protected $connection;
-
-       /**
-        * Constructs an Nntp object
-        *
-        * @param string $hostname
-        * @param int $port
-        */
-       public function __construct($hostname, $port = 119)
-       {
-               $errno = $errstr = null;
-               $this->connection = @fsockopen($hostname, $port, $errno, 
$errstr, 30);
-
-               if (!$this->connection) {
-                       throw new \RuntimeException(
-                               "Unable to connect to {$hostname} on port 
{$port}: {$errstr}"
-                       );
-               }
-
-               $hello = fgets($this->connection);
-               $responseCode = substr($hello, 0, 3);
-
-               switch ($responseCode) {
-                       case 400:
-                       case 502:
-                               throw new \RuntimeException('Service 
unavailable');
-                               break;
-                       case 200:
-                       case 201:
-                       default:
-                               // Successful connection
-                               break;
-               }
-       }
-
-       /**
-        * Closes the NNTP connection when the object is destroyed
-        */
-       public function __destruct()
-       {
-               $this->sendCommand('QUIT', 205);
-               fclose($this->connection);
-               $this->connection = null;
-       }
-
-       /**
-        * Sends the LIST command to the server and returns an array of 
newsgroups
-        *
-        * @return array
-        */
-       public function listGroups()
-       {
-               $list = [];
-               $response = $this->sendCommand('LIST', 215);
-
-               if ($response !== false) {
-                       while ($line = fgets($this->connection)) {
-                               if ($line == ".\r\n") {
-                                       break;
-                               }
-
-                               $line = rtrim($line);
-                               list($group, $high, $low, $status) = explode(' 
', $line);
-
-                               $list[$group] = [
-                                       'high' => $high,
-                                       'low' => $low,
-                                       'status' => $status,
-                               ];
-                       }
-               }
-
-               return $list;
-       }
-
-       /**
-        * Sets the active group at the server and returns details about the 
group
-        *
-        * @param string $group Name of the group to set as the active group
-        * @return array
-        * @throws \RuntimeException
-        */
-       public function selectGroup($group)
-       {
-               $response = $this->sendCommand("GROUP {$group}", 211);
-               
-               if ($response !== false) {
-                       list($number, $low, $high, $group) = explode(' ', 
$response);
-
-                       return [
-                               'group' => $group,
-                               'articlesCount' => $number,
-                               'low' => $low,
-                               'high' => $high,
-                       ];
-               }
-
-               throw new \RuntimeException('Failed to get info on group');
-       }
-
-       /**
-        * Returns an overview of the selected articles from the specified group
-        *
-        * @param string $group The name of the group to select
-        * @param int $start The number of the article to start from
-        * @param int $pageSize The number of articles to return
-        * @return array
-        */
-       public function getArticlesOverview($group, $start, $pageSize = 20)
-       {
-               $groupDetails = $this->selectGroup($group);
-
-               $pageSize = $pageSize - 1;
-               $high = $groupDetails['high'];
-               $low = $groupDetails['low'];
-
-               if (!$start || $start > $high - $pageSize || $start < $low) {
-                       $start = $high - $low > $pageSize ? $high - $pageSize : 
$low;
-               }
-
-               $end = min($high, $start + $pageSize);
-
-               $overview = [
-                       'group' => $groupDetails + ['start' => $start],
-                       'articles' => [],
-               ];
-
-               $response = $this->sendCommand("XOVER {$start}-{$end}", 224);
-
-               while ($line = fgets($this->connection)) {
-                       if ($line == ".\r\n") {
-                               break;
-                       }
-
-                       $line = rtrim($line);
-                       list($n, $subject, $author, $date, $messageId, 
$references, $lines, $extra) = explode("\t", $line, 9);
-
-                       $overview['articles'][$n] = [
-                               'subject' => $subject,
-                               'author' => $author,
-                               'date' => $date,
-                               'messageId' => $messageId,
-                               'references' => $references,
-                               'lines' => $lines,
-                               'extra' => $extra,
-                       ];
-               }
-
-               return $overview;
-       }
-
-       /**
-        * Returns the full content of the specified article (headers and body)
-        *
-        * @param int $articleId
-        * @param string|null $group
-        * @return string
-        */
-       public function readArticle($articleId, $group = null)
-       {
-               if ($group) {
-                       $groupDetails = $this->selectGroup($group);
-               }
-
-               $article = '';
-
-               try {
-                       $response = $this->sendCommand("ARTICLE {$articleId}", 
220);
-               } catch (\RuntimeException $e) {
-                       return null;
-               }
-
-               while ($line = fgets($this->connection)) {
-                       if ($line == ".\r\n") {
-                               break;
-                       }
-
-                       $article .= $line;
-               }
-
-               return $article;
-       }
-
-       /**
-        * Performs a lookup on the $messageId to find its group and article ID
-        *
-        * @param string $messageId
-        * @return array
-        */
-       public function xpath($messageId)
-       {
-               $response = $this->sendCommand("XPATH {$messageId}", 223);
-               list($group, $articleId) = explode('/', $response);
-
-               return [
-                       'messageId' => $messageId,
-                       'group' => $group,
-                       'articleId' => $articleId,
-               ];
-       }
-
-       /**
-        * Sends a command to the server and checks the expected response code
-        *
-        * @param string $command
-        * @param int $expected The successful response code expected
-        * @return string
-        */
-       protected function sendCommand($command, $expected)
-       {
-               fwrite($this->connection, "$command\r\n");
-               $result = fgets($this->connection);
-               list($code, $response) = explode(' ', $result, 2);
-
-               if ($code == $expected) {
-                       return rtrim($response);
-               }
-
-               throw new \RuntimeException(
-                       "Expected response code of {$expected} but received 
{$code} for command `{$command}'"
-               );
-       }
+    /**
+     * @var resource
+     */
+    protected $connection;
+
+    /**
+     * Constructs an Nntp object
+     *
+     * @param string $hostname
+     * @param int $port
+     */
+    public function __construct($hostname, $port = 119)
+    {
+        $errno = $errstr = null;
+        $this->connection = @fsockopen($hostname, $port, $errno, $errstr, 30);
+
+        if (!$this->connection) {
+            throw new \RuntimeException(
+                "Unable to connect to {$hostname} on port {$port}: {$errstr}"
+            );
+        }
+
+        $hello = fgets($this->connection);
+        $responseCode = substr($hello, 0, 3);
+
+        switch ($responseCode) {
+            case 400:
+            case 502:
+                throw new \RuntimeException('Service unavailable');
+                break;
+            case 200:
+            case 201:
+            default:
+                // Successful connection
+                break;
+        }
+    }
+
+    /**
+     * Closes the NNTP connection when the object is destroyed
+     */
+    public function __destruct()
+    {
+        $this->sendCommand('QUIT', 205);
+        fclose($this->connection);
+        $this->connection = null;
+    }
+
+    /**
+     * Sends the LIST command to the server and returns an array of newsgroups
+     *
+     * @return array
+     */
+    public function listGroups()
+    {
+        $list = [];
+        $response = $this->sendCommand('LIST', 215);
+
+        if ($response !== false) {
+            while ($line = fgets($this->connection)) {
+                if ($line == ".\r\n") {
+                    break;
+                }
+
+                $line = rtrim($line);
+                list($group, $high, $low, $status) = explode(' ', $line);
+
+                $list[$group] = [
+                    'high' => $high,
+                    'low' => $low,
+                    'status' => $status,
+                ];
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Sets the active group at the server and returns details about the group
+     *
+     * @param string $group Name of the group to set as the active group
+     * @return array
+     * @throws \RuntimeException
+     */
+    public function selectGroup($group)
+    {
+        $response = $this->sendCommand("GROUP {$group}", 211);
+
+        if ($response !== false) {
+            list($number, $low, $high, $group) = explode(' ', $response);
+
+            return [
+                'group' => $group,
+                'articlesCount' => $number,
+                'low' => $low,
+                'high' => $high,
+            ];
+        }
+
+        throw new \RuntimeException('Failed to get info on group');
+    }
+
+    /**
+     * Returns an overview of the selected articles from the specified group
+     *
+     * @param string $group The name of the group to select
+     * @param int $start The number of the article to start from
+     * @param int $pageSize The number of articles to return
+     * @return array
+     */
+    public function getArticlesOverview($group, $start, $pageSize = 20)
+    {
+        $groupDetails = $this->selectGroup($group);
+
+        $pageSize = $pageSize - 1;
+        $high = $groupDetails['high'];
+        $low = $groupDetails['low'];
+
+        if (!$start || $start > $high - $pageSize || $start < $low) {
+            $start = $high - $low > $pageSize ? $high - $pageSize : $low;
+        }
+
+        $end = min($high, $start + $pageSize);
+
+        $overview = [
+            'group' => $groupDetails + ['start' => $start],
+            'articles' => [],
+        ];
+
+        $response = $this->sendCommand("XOVER {$start}-{$end}", 224);
+
+        while ($line = fgets($this->connection)) {
+            if ($line == ".\r\n") {
+                break;
+            }
+
+            $line = rtrim($line);
+            list($n, $subject, $author, $date, $messageId, $references, 
$lines, $extra) = explode("\t", $line, 9);
+
+            $overview['articles'][$n] = [
+                'subject' => $subject,
+                'author' => $author,
+                'date' => $date,
+                'messageId' => $messageId,
+                'references' => $references,
+                'lines' => $lines,
+                'extra' => $extra,
+            ];
+        }
+
+        return $overview;
+    }
+
+    /**
+     * Returns the full content of the specified article (headers and body)
+     *
+     * @param int $articleId
+     * @param string|null $group
+     * @return string
+     */
+    public function readArticle($articleId, $group = null)
+    {
+        if ($group) {
+            $groupDetails = $this->selectGroup($group);
+        }
+
+        $article = '';
+
+        try {
+            $response = $this->sendCommand("ARTICLE {$articleId}", 220);
+        } catch (\RuntimeException $e) {
+            return null;
+        }
+
+        while ($line = fgets($this->connection)) {
+            if ($line == ".\r\n") {
+                break;
+            }
+
+            $article .= $line;
+        }
+
+        return $article;
+    }
+
+    /**
+     * Performs a lookup on the $messageId to find its group and article ID
+     *
+     * @param string $messageId
+     * @return array
+     */
+    public function xpath($messageId)
+    {
+        $response = $this->sendCommand("XPATH {$messageId}", 223);
+        list($group, $articleId) = explode('/', $response);
+
+        return [
+            'messageId' => $messageId,
+            'group' => $group,
+            'articleId' => $articleId,
+        ];
+    }
+
+    /**
+     * Sends a command to the server and checks the expected response code
+     *
+     * @param string $command
+     * @param int $expected The successful response code expected
+     * @return string
+     */
+    protected function sendCommand($command, $expected)
+    {
+        fwrite($this->connection, "$command\r\n");
+        $result = fgets($this->connection);
+        list($code, $response) = explode(' ', $result, 2);
+
+        if ($code == $expected) {
+            return rtrim($response);
+        }
+
+        throw new \RuntimeException(
+            "Expected response code of {$expected} but received {$code} for 
command `{$command}'"
+        );
+    }
 }
diff --git a/lib/common.php b/lib/common.php
new file mode 100644
index 0000000..149ab12
--- /dev/null
+++ b/lib/common.php
@@ -0,0 +1,219 @@
+<?php
+
+/* Prevents the poor mail server from suffering if it receives a message with 
many references */
+/* (References: <xxx> or In-Reply-To: <xxx>) */
+define('REFERENCES_LIMIT', 20);
+
+function error($str)
+{
+    head("PHP news : error");
+    echo "<section class=\"content\"><blockquote><strong>Error:</strong> ",
+       to_utf8($str), "</blockquote></section>\n";
+    foot();
+    die();
+}
+
+function head($title = "PHP Mailing Lists (PHP News)")
+{
+    header("Content-type: text/html; charset=utf-8");
+
+    ?>
+<!doctype html>
+<html lang="en">
+ <head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title><?php echo htmlspecialchars($title); ?></title>
+  <link rel="stylesheet" href="/fonts/Fira/fira.css" type="text/css" />
+  <link rel="stylesheet" href="/style.css" type="text/css" />
+  <link rel="shortcut icon" href="/favicon.ico">
+ </head>
+ <body>
+  <header class="header">
+   <nav class="header-inner">
+    <a href="/" class="header-brand">
+      <img src="//php.net/images/logos/php-logo.svg" class="header-brand-img" 
alt="PHP" height="24" width="48">
+      <span class="header-brand-text">lists</span>
+    </a>
+    <ul class="header-menu">
+      <li class="header-menu-item">
+        <a class="header-menu-item-link" 
href="https://php.net/downloads.php";>Downloads</a>
+      </li>
+      <li class="header-menu-item">
+        <a class="header-menu-item-link" 
href="https://php.net/docs.php";>Documentation</a>
+      </li>
+      <li class="header-menu-item">
+        <a class="header-menu-item-link" 
href="https://php.net/get-involved.php";>Get Involved</a>
+      </li>
+      <li class="header-menu-item mod-active">
+        <a class="header-menu-item-link" 
href="https://php.net/support.php";>Help</a>
+      </li>
+     </ul>
+     <form class="search-form" action="https://php.net/search.php";>
+      <input class="search-input" value="" name="pattern" placeholder="Search">
+     </form>
+    <div class="menu-icon" 
onclick="document.querySelector('.menu-mobile').classList.toggle('hide')">☰ 
MENU</div>
+     <ul class="menu-mobile hide">
+      <li class="menu-mobile-item">
+        <a class="menu-mobile-item-link" 
href="https://php.net/downloads.php";>Downloads</a>
+      </li>
+      <li class="menu-mobile-item">
+        <a class="menu-mobile-item-link" 
href="https://php.net/docs.php";>Documentation</a>
+      </li>
+      <li class="menu-mobile-item">
+        <a class="menu-mobile-item-link" 
href="https://php.net/get-involved.php";>Get Involved</a>
+      </li>
+      <li class="menu-mobile-item mod-active">
+        <a class="menu-mobile-item-link" 
href="https://php.net/support.php";>Help</a>
+      </li>
+     </ul>
+   </nav>
+  </header>
+    <?php
+}
+
+function foot()
+{
+    ?>
+
+ <footer class="footer">
+    <ul class="footer-nav">
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" href="https://php.net/copyright.php";>
+         Copyright ©2001-<?php echo date('Y'); ?> The PHP Group
+       </a>
+     </li>
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" href="https://php.net/my.php";>My 
PHP.net</a>
+     </li>
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" 
href="https://php.net/contact.php";>Contact</a>
+     </li>
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" href="https://php.net/sites.php";>Other 
PHP.net sites</a>
+     </li>
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" 
href="https://php.net/mirrors.php";>Mirror sites</a>
+     </li>
+     <li class="footer-nav-item">
+       <a class="footer-nav-item-link" 
href="https://php.net/privacy.php";>Privacy policy</a>
+     </li>
+    </ul>
+ </footer>
+ </body>
+</html>
+    <?php
+}
+
+function to_utf8($str, $charset = 'iso-8859-1')
+{
+    $n = iconv($charset, 'utf-8', $str);
+    if ($n === false) {
+        return $str;
+    }
+    return $n;
+}
+
+function decode_header($charset, $encoding, $text)
+{
+    if (strtolower($encoding) == "b") {
+        $text = base64_decode($text);
+    } else {
+        $text = quoted_printable_decode($text);
+    }
+    return to_utf8($text, $charset);
+}
+
+function recode_header($header, $basecharset)
+{
+    if (strpos($header, "=?") === false) {
+        return to_utf8($header, $basecharset);
+    }
+    return preg_replace_callback(
+        "/=\\?(.+?)\\?([qb])\\?(.+?)(\\?=|$)/i",
+        function ($m) {
+            return decode_header($m[1], $m[2], $m[3]);
+        },
+        $header
+    );
+}
+
+/* Email spam protection (taken from php-bugs-web) */
+function spam_protect($txt)
+{
+    $translate = array('@' => ' at ', '.' => ' dot ');
+
+    /* php.net addresses are not protected! */
+    if (preg_match('/^(.+)@php\.net/i', $txt)) {
+        return $txt;
+    } else {
+        return strtr($txt, $translate);
+    }
+}
+
+
+# this turns some common forms of email addresses into mailto: links
+function format_author($a, $charset = 'iso-8859-1')
+{
+    $a = recode_header($a, $charset);
+    if (preg_match("/^\s*(.+)\s+\\(\"?(.+?)\"?\\)\s*$/", $a, $ar)) {
+        return "<a href=\"mailto:"; .
+            htmlspecialchars(urlencode(spam_protect($ar[1])), ENT_QUOTES, 
"UTF-8") .
+            "\" class=\"email fn n\">" .
+            str_replace(" ", "&nbsp;", htmlspecialchars($ar[2], ENT_QUOTES, 
"UTF-8")) . "</a>";
+    }
+    if (preg_match("/^\s*\"?(.+?)\"?\s*<(.+)>\s*$/", $a, $ar)) {
+        return "<a href=\"mailto:"; .
+            htmlspecialchars(urlencode(spam_protect($ar[2])), ENT_QUOTES, 
"UTF-8") .
+            "\" class=\"email fn n\">" .
+            str_replace(" ", "&nbsp;", htmlspecialchars($ar[1], ENT_QUOTES, 
"UTF-8")) . "</a>";
+    }
+    if (strpos("@", $a) !== false) {
+        $a = spam_protect($a);
+        return "<a href=\"mailto:"; . htmlspecialchars(urlencode($a), 
ENT_QUOTES, "UTF-8") .
+            "\" class=\"email fn n\">" . htmlspecialchars($a, ENT_QUOTES, 
"UTF-8") . "</a>";
+    }
+    return str_replace(" ", "&nbsp;", htmlspecialchars($a, ENT_QUOTES, 
"UTF-8"));
+}
+
+function format_subject($s, $charset = 'iso-8859-1')
+{
+    global $article;
+    $s = recode_header($s, $charset);
+
+    if ((($pos = strpos($s, '[PHP')) !== false || ($pos = strpos($s, '[PEAR')) 
!== false)) {
+        if (($end_pos = strpos($s, ']', $pos)) !== false) {
+            $s = ltrim(substr_replace($s, '', $pos, $end_pos - $pos + 1));
+        }
+    }
+
+    // make this look better on the preview page..
+    if (strlen($s) > 150 && !isset($article)) {
+        $s = substr($s, 0, 150) . "...";
+    } else {
+        $s = wordwrap($s, 150);
+    }
+    return nl2br(htmlspecialchars($s, ENT_QUOTES, "UTF-8"));
+}
+
+
+function format_title($s, $charset = 'iso-8859-1')
+{
+    global $article;
+    $s = recode_header($s, $charset);
+    $s = preg_replace("/^(Re: *)?\[(PHP|PEAR)(-.*)?\] /i", "\\1", $s);
+    // make this look better on the preview page..
+    if (strlen($s) > 150 && !isset($article)) {
+        $s = substr($s, 0, 150) . "...";
+    } else {
+        $s = wordwrap($s, 150);
+    }
+    return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
+}
+
+function format_date($d)
+{
+    $d = strtotime($d);
+    $d = gmdate('r', $d);
+    return str_replace(" ", "&nbsp;", $d);
+}
diff --git a/lib/config.php b/lib/config.php
new file mode 100644
index 0000000..71a3835
--- /dev/null
+++ b/lib/config.php
@@ -0,0 +1,6 @@
+<?php
+
+$NNTP_HOST = 'localhost';
+if (getenv('NNTP_HOST')) {
+    $NNTP_HOST = getenv('NNTP_HOST');
+}
diff --git a/lib/fMailbox.php b/lib/fMailbox.php
index 04f05af..78a5169 100644
--- a/lib/fMailbox.php
+++ b/lib/fMailbox.php
@@ -1,4 +1,7 @@
 <?php
+
+namespace Flourish;
+
 /**
  * This is a heavily-trimmed version of Will Bond's Flourish library fMailbox
  * class. It is based on the version of the file located here:
@@ -36,528 +39,519 @@
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  * THE SOFTWARE.
  */
-class fMailbox
+class Mailbox
 {
-       /**
-        * Takes a date, removes comments and cleans up some common formatting 
inconsistencies
-        *
-        * @param string $date  The date to clean
-        * @return string  The cleaned date
-        */
-       private static function cleanDate($date)
-       {
-               $date = preg_replace('#\([^)]+\)#', ' ', trim($date));
-               $date = preg_replace('#\s+#', ' ', $date);
-               $date = preg_replace('#(\d+)-([a-z]+)-(\d{4})#i', '\1 \2 \3', 
$date);
-               $date = preg_replace('#^[a-z]+\s*,\s*#i', '', trim($date));
-               return trim($date);
-       }
-
-       /**
-        * Decodes encoded-word headers of any encoding into raw UTF-8
-        *
-        * @param string $text  The header value to decode
-        * @return string  The decoded UTF-8
-        */
-       private static function decodeHeader($text)
-       {
-               $parts = preg_split('#(=\?[^\?]+\?[QB]\?[^\?]+\?=)#i', $text, 
-1, PREG_SPLIT_DELIM_CAPTURE);
-
-               $part_with_encoding = array();
-               $output = '';
-               foreach ($parts as $part) {
-                       if ($part === '') {
-                               continue;
-                       }
-
-                       if 
(preg_match_all('#=\?([^\?]+)\?([QB])\?([^\?]+)\?=#i', $part, $matches, 
PREG_SET_ORDER)) {
-                               foreach ($matches as $match) {
-                                       if (strtoupper($match[2]) == 'Q') {
-                                               $part_string = 
rawurldecode(strtr(
-                                                       $match[3],
-                                                       array(
-                                                               '=' => '%',
-                                                               '_' => ' '
-                                                       )
-                                               ));
-                                       } else {
-                                               $part_string = 
base64_decode($match[3]);
-                                       }
-                                       $lower_encoding = strtolower($match[1]);
-                                       $last_key = count($part_with_encoding) 
- 1;
-                                       if 
(isset($part_with_encoding[$last_key]) && 
$part_with_encoding[$last_key]['encoding'] == $lower_encoding) {
-                                               
$part_with_encoding[$last_key]['string'] .= $part_string;
-                                       } else {
-                                               $part_with_encoding[] = 
array('encoding' => $lower_encoding, 'string' => $part_string);
-                                       }
-                               }
-
-                       } else {
-                               $last_key = count($part_with_encoding) - 1;
-                               if (isset($part_with_encoding[$last_key]) && 
$part_with_encoding[$last_key]['encoding'] == 'iso-8859-1') {
-                                       
$part_with_encoding[$last_key]['string'] .= $part;
-                               } else {
-                                       $part_with_encoding[] = 
array('encoding' => 'iso-8859-1', 'string' => $part);
-                               }
-                       }
-               }
-
-               foreach ($part_with_encoding as $part) {
-                       $output .= self::iconv($part['encoding'], 'UTF-8', 
$part['string']);
-               }
-
-               return $output;
-       }
-
-       /**
-        * Handles an individual part of a multipart message
-        *
-        * @param array  $info       An array of information about the message
-        * @param array  $structure  An array describing the structure of the 
message
-        * @return array  The modified $info array
-        */
-       private static function handlePart($info, $structure)
-       {
-               if ($structure['type'] == 'multipart') {
-                       foreach ($structure['parts'] as $part) {
-                               $info = self::handlePart($info, $part);
-                       }
-                       return $info;
-               }
-
-               if ($structure['type'] == 'application' && 
in_array($structure['subtype'], array('pkcs7-mime', 'x-pkcs7-mime'))) {
-                       $to = null;
-                       if (isset($info['headers']['to'][0])) {
-                               $to = $info['headers']['to'][0]['mailbox'];
-                               if (!empty($info['headers']['to'][0]['host'])) {
-                                       $to .= '@' . 
$info['headers']['to'][0]['host'];
-                               }
-                       }
-               }
-
-               if ($structure['type'] == 'application' && 
in_array($structure['subtype'], array('pkcs7-signature', 'x-pkcs7-signature'))) 
{
-                       $from = null;
-                       if (isset($info['headers']['from'])) {
-                               $from = $info['headers']['from']['mailbox'];
-                               if (!empty($info['headers']['from']['host'])) {
-                                       $from .= '@' . 
$info['headers']['from']['host'];
-                               }
-                       }
-               }
-
-               $data = $structure['data'];
-
-               if ($structure['encoding'] == 'base64') {
-                       $content = '';
-                       foreach (explode("\r\n", $data) as $line) {
-                               $content .= base64_decode($line);
-                       }
-               } elseif ($structure['encoding'] == 'quoted-printable') {
-                       $content = quoted_printable_decode($data);
-               } else {
-                       $content = $data;
-               }
-
-               if ($structure['type'] == 'text') {
-                       $charset = 'iso-8859-1';
-                       foreach ($structure['type_fields'] as $field => $value) 
{
-                               if (strtolower($field) == 'charset') {
-                                       $charset = $value;
-                                       break;
-                               }
-                       }
-                       $content = self::iconv($charset, 'UTF-8', $content);
-                       if ($structure['subtype'] == 'html') {
-                               $content = 
preg_replace('#(content=(["\'])text/html\s*;\s*charset=(["\']?))' . 
preg_quote($charset, '#') . '(\3\2)#i', '\1utf-8\4', $content);
-                       }
-               }
-
-               // This indicates a content-id which is used for 
multipart/related
-               if ($structure['content_id']) {
-                       if (!isset($info['related'])) {
-                               $info['related'] = array();
-                       }
-                       $cid = $structure['content_id'][0] == '<' ? 
substr($structure['content_id'], 1, -1) : $structure['content_id'];
-                       $info['related']['cid:' . $cid] = array(
-                               'mimetype' => $structure['type'] . '/' . 
$structure['subtype'],
-                               'data'     => $content
-                       );
-                       return $info;
-               }
-
-
-               $has_disposition = !empty($structure['disposition']);
-               $is_text         = $structure['type'] == 'text' && 
$structure['subtype'] == 'plain';
-               $is_html         = $structure['type'] == 'text' && 
$structure['subtype'] == 'html';
-
-               // If the part doesn't have a disposition and is not the 
default text or html, set the disposition to inline
-               if (!$has_disposition && ((!$is_text || !empty($info['text'])) 
&& (!$is_html || !empty($info['html'])))) {
-                       $is_web_image = $structure['type'] == 'image' && 
in_array($structure['subtype'], array('gif', 'png', 'jpeg', 'pjpeg'));
-                       $structure['disposition'] = $is_text || $is_html || 
$is_web_image ? 'inline' : 'attachment';
-                       $structure['disposition_fields'] = array();
-                       $has_disposition = true;
-               }
-
-
-               // Attachments or inline content
-               if ($has_disposition) {
-
-                       $filename = '';
-                       foreach ($structure['disposition_fields'] as $field => 
$value) {
-                               if (strtolower($field) == 'filename') {
-                                       $filename = $value;
-                                       break;
-                               }
-                       }
-                       foreach ($structure['type_fields'] as $field => $value) 
{
-                               if (strtolower($field) == 'name') {
-                                       $filename = $value;
-                                       break;
-                               }
-                       }
-
-                       // This automatically handles primary content that has 
a content-disposition header on it
-                       if ($structure['disposition'] == 'inline' && $filename 
=== '') {
-                               if ($is_text && !isset($info['text'])) {
-                                       $info['text'] = $content;
-                                       return $info;
-                               }
-                               if ($is_html && !isset($info['html'])) {
-                                       $info['html'] = $content;
-                                       return $info;
-                               }
-                       }
-
-                       if (!isset($info[$structure['disposition']])) {
-                               $info[$structure['disposition']] = array();
-                       }
-
-                       $info[$structure['disposition']][] = array(
-                               'filename' => $filename,
-                               'mimetype' => $structure['type'] . '/' . 
$structure['subtype'],
-                               'data'     => $content,
-                               'description' => $structure['description'],
-                       );
-                       return $info;
-               }
-
-               if ($is_text) {
-                       $info['text'] = $content;
-                       return $info;
-               }
-
-               if ($is_html) {
-                       $info['html'] = $content;
-                       return $info;
-               }
-       }
-
-       /**
-        * This works around a bug in MAMP 1.9.4+ and PHP 5.3 where iconv()
-        * does not seem to properly assign the return value to a variable, but
-        * does work when returning the value.
-        *
-        * @param string $in_charset   The incoming character encoding
-        * @param string $out_charset  The outgoing character encoding
-        * @param string $string       The string to convert
-        * @return string  The converted string
-        */
-       private static function iconv($in_charset, $out_charset, $string)
-       {
-               return iconv($in_charset, $out_charset, $string);
-       }
-
-       /**
-        * Parses a string representation of an email into the persona, mailbox 
and host parts
-        *
-        * @param  string $string  The email string to parse
-        * @return array  An associative array with the key `mailbox`, and 
possibly `host` and `personal`
-        */
-       private static function parseEmail($string)
-       {
-               $email_regex = 
'((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")(?:\.[ 
\t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ 
\t]*))*)@((?:[a-z0-9\\-]+\.)+[a-z]{2,}|\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\])';
-               $name_regex  = '((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ 
\t]*|"[^"\\\\\n\r]+"[ \t]*)(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ 
\t]*|"[^"\\\\\n\r]+"[ \t]*))*)';
-
-               if (preg_match('~^[ \t]*' . $name_regex . '[ \t]*<[ \t]*' . 
$email_regex . '[ \t]*>[ \t]*$~ixD', $string, $match)) {
-                       $match[1] = trim($match[1]);
-                       if ($match[1][0] == '"' && substr($match[1], -1) == 
'"') {
-                               $match[1] = substr($match[1], 1, -1);
-                       }
-                       return array(
-                               'personal' => self::decodeHeader($match[1]),
-                               'mailbox' => self::decodeHeader($match[2]),
-                               'host' => self::decodeHeader($match[3]),
-                               'raw' => $string,
-                       );
-
-               } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . 
'(?:[ \t]*>)?[ \t]*$~ixD', $string, $match)) {
-                       return array(
-                               'mailbox' => self::decodeHeader($match[1]),
-                               'host' => self::decodeHeader($match[2]),
-                               'raw' => $string,
-                       );
-
-                       // This handles the outdated practice of including the 
personal
-                       // part of the email in a comment after the email 
address
-               } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . 
'(?:[ \t]*>)?[ \t]*\(([^)]+)\)[ \t]*$~ixD', $string, $match)) {
-                       $match[3] = trim($match[1]);
-                       if ($match[3][0] == '"' && substr($match[3], -1) == 
'"') {
-                               $match[3] = substr($match[3], 1, -1);
-                       }
-
-                       return array(
-                               'personal' => self::decodeHeader($match[3]),
-                               'mailbox' => self::decodeHeader($match[1]),
-                               'host' => self::decodeHeader($match[2]),
-                               'raw' => $string,
-                       );
-               }
-
-               if (strpos($string, '@') !== false) {
-                       list ($mailbox, $host) = explode('@', $string, 2);
-                       return array(
-                               'mailbox' => self::decodeHeader($mailbox),
-                               'host' => self::decodeHeader($host),
-                               'raw' => $string,
-                       );
-               }
-
-               return array(
-                       'mailbox' => self::decodeHeader($string),
-                       'host' => '',
-                       'raw' => $string,
-               );
-       }
-
-       /**
-        * Parses full email headers into an associative array
-        *
-        * @param  string $headers  The header to parse
-        * @param  string $filter   Remove any headers that match this
-        * @return array  The parsed headers
-        */
-       private static function parseHeaders($headers, $filter = null)
-       {
-               $headers = trim($headers);
-               if (!strlen($headers)) {
-                       return array();
-               }
-               $header_lines = preg_split("#\r\n(?!\s)#", $headers);
-
-               $single_email_fields    = array('from', 'sender', 'reply-to');
-               $multi_email_fields     = array('to', 'cc');
-               $additional_info_fields = array('content-type', 
'content-disposition');
-
-               $parsed_headers = array();
-               foreach ($header_lines as $header_line) {
-                       $header_line = preg_replace("#\r\n\s+#", ' ', 
$header_line);
-                       $header_line = trim($header_line);
-
-                       list ($header, $value) = preg_split('#:\s*#', 
$header_line, 2);
-                       $header = strtolower($header);
-
-                       if ($filter !== null && strpos($header, $filter) !== 
false) {
-                               continue;
-                       }
-
-                       $is_single_email          = in_array($header, 
$single_email_fields);
-                       $is_multi_email           = in_array($header, 
$multi_email_fields);
-                       $is_additional_info_field = in_array($header, 
$additional_info_fields);
-
-                       if ($is_additional_info_field) {
-                               $pieces = preg_split('#;\s*#', $value, 2);
-                               $value = $pieces[0];
-
-                               $parsed_headers[$header] = array('value' => 
self::decodeHeader($value));
-
-                               $fields = array();
-                               if (!empty($pieces[1])) {
-                                       
preg_match_all('#(\w+)=("([^"]+)"|([^\s;]+))(?=;|$)#', $pieces[1], $matches, 
PREG_SET_ORDER);
-                                       foreach ($matches as $match) {
-                                               $fields[strtolower($match[1])] 
= self::decodeHeader(!empty($match[4]) ? $match[4] : $match[3]);
-                                       }
-                               }
-                               $parsed_headers[$header]['fields'] = $fields;
-
-                       } elseif ($is_single_email) {
-                               $parsed_headers[$header] = 
self::parseEmail($value);
-
-                       } elseif ($is_multi_email) {
-                               $strings = array();
-
-                               preg_match_all('#"[^"]+?"#', $value, $matches, 
PREG_SET_ORDER);
-                               foreach ($matches as $i => $match) {
-                                       $strings[] = $match[0];
-                                       $value = preg_replace('#' . 
preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
-                               }
-                               preg_match_all('#\([^)]+?\)#', $value, 
$matches, PREG_SET_ORDER);
-                               foreach ($matches as $i => $match) {
-                                       $strings[] = $match[0];
-                                       $value = preg_replace('#' . 
preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
-                               }
-
-                               $emails = explode(',', $value);
-                               array_map('trim', $emails);
-                               foreach ($strings as $i => $string) {
-                                       $emails = preg_replace(
-                                               '#:string' . ($i+1) . '\b#',
-                                               strtr($string, array('\\' => 
'\\\\', '$' => '\\$')),
-                                               $emails,
-                                               1
-                                       );
-                               }
-
-                               $parsed_headers[$header] = array();
-                               foreach ($emails as $email) {
-                                       $parsed_headers[$header][] = 
self::parseEmail($email);
-                               }
-
-                       } elseif ($header == 'references') {
-                               $parsed_headers[$header] = 
array_map(array('fMailbox', 'decodeHeader'), preg_split('#(?<=>)\s+(?=<)#', 
$value));
-
-                       } elseif ($header == 'received') {
-                               if (!isset($parsed_headers[$header])) {
-                                       $parsed_headers[$header] = array();
-                               }
-                               $parsed_headers[$header][] = 
preg_replace('#\s+#', ' ', self::decodeHeader($value));
-
-                       } else {
-                               $parsed_headers[$header] = 
self::decodeHeader($value);
-                       }
-               }
-
-               return $parsed_headers;
-       }
-
-       /**
-        * Parses a MIME message into an associative array of information
-        *
-        * The output includes the following keys:
-        *
-        *  - `'received'`: The date the message was received by the server
-        *  - `'headers'`: An associative array of mail headers, the keys are 
the header names, in lowercase
-        *
-        * And one or more of the following:
-        *
-        *  - `'text'`: The plaintext body
-        *  - `'html'`: The HTML body
-        *  - `'attachment'`: An array of attachments, each containing:
-        *   - `'filename'`: The name of the file
-        *   - `'mimetype'`: The mimetype of the file
-        *   - `'data'`: The raw contents of the file
-        *  - `'inline'`: An array of inline files, each containing:
-        *   - `'filename'`: The name of the file
-        *   - `'mimetype'`: The mimetype of the file
-        *   - `'data'`: The raw contents of the file
-        *  - `'related'`: An associative array of related files, such as 
embedded images, with the key `'cid:{content-id}'` and an array value 
containing:
-        *   - `'mimetype'`: The mimetype of the file
-        *   - `'data'`: The raw contents of the file
-        *  - `'verified'`: If the message contents were verified via an S/MIME 
certificate - if not verified the smime.p7s will be listed as an attachment
-        *  - `'decrypted'`: If the message contents were decrypted via an 
S/MIME private key - if not decrypted the smime.p7m will be listed as an 
attachment
-        *
-        * All values in `headers`, `text` and `body` will have been decoded to
-        * UTF-8. Files in the `attachment`, `inline` and `related` array will 
all
-        * retain their original encodings.
-        *
-        * @param string  $message           The full source of the email 
message
-        * @param boolean $convert_newlines  If `\r\n` should be converted to 
`\n` in the `text` and `html` parts the message
-        * @return array  The parsed email message - see method description for 
details
-        */
-       public static function parseMessage($message, $convert_newlines = false)
-       {
-               $info = array();
-               list ($headers, $body)   = explode("\r\n\r\n", $message, 2);
-               $parsed_headers          = self::parseHeaders($headers);
-               $info['received']        = 
self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', 
$parsed_headers['received'][0]));
-               $info['headers']         = array();
-               foreach ($parsed_headers as $header => $value) {
-                       if (substr($header, 0, 8) == 'content-') {
-                               continue;
-                       }
-                       $info['headers'][$header] = $value;
-               }
-               $info['raw_headers'] = $headers;
-               $info['raw_message'] = $message;
-
-               $info = self::handlePart($info, self::parseStructure($body, 
$parsed_headers));
-               unset($info['raw_message']);
-               unset($info['raw_headers']);
-
-               if ($convert_newlines) {
-                       if (isset($info['text'])) {
-                               $info['text'] = str_replace("\r\n", "\n", 
$info['text']);
-                       }
-                       if (isset($info['html'])) {
-                               $info['html'] = str_replace("\r\n", "\n", 
$info['html']);
-                       }
-               }
-
-               if (isset($info['text'])) {
-                       $info['text'] = preg_replace('#\r?\n$#D', '', 
$info['text']);
-               }
-               if (isset($info['html'])) {
-                       $info['html'] = preg_replace('#\r?\n$#D', '', 
$info['html']);
-               }
-
-               return $info;
-       }
-
-       /**
-        * Takes the raw contents of a MIME message and creates an array that
-        * describes the structure of the message
-        *
-        * @param string $data     The contents to get the structure of
-        * @param string $headers  The parsed headers for the message - if not 
present they will be extracted from the `$data`
-        * @return array  The multi-dimensional, associative array containing 
the message structure
-        */
-       private static function parseStructure($data, $headers = null)
-       {
-               if (!$headers) {
-                       list ($headers, $data) = preg_split("#^\r\n|\r\n\r\n#", 
$data, 2);
-                       $headers = self::parseHeaders($headers);
-               }
-
-               if (!isset($headers['content-type'])) {
-                       $headers['content-type'] = array(
-                               'value'  => 'text/plain',
-                               'fields' => array()
-                       );
-               }
-
-               list ($type, $subtype) = explode('/', 
strtolower($headers['content-type']['value']), 2);
-
-               if ($type == 'multipart') {
-                       $structure    = array(
-                               'type'    => $type,
-                               'subtype' => $subtype,
-                               'parts'   => array()
-                       );
-                       $boundary     = 
$headers['content-type']['fields']['boundary'];
-                       $start_pos    = strpos($data, '--' . $boundary) + 
strlen($boundary) + 4;
-                       $end_pos      = strrpos($data, '--' . $boundary . '--') 
- 2;
-                       $sub_contents = explode("\r\n--" . $boundary . "\r\n", 
substr(
-                               $data,
-                               $start_pos,
-                               $end_pos - $start_pos
-                       ));
-                       foreach ($sub_contents as $sub_content) {
-                               $structure['parts'][] = 
self::parseStructure($sub_content);
-                       }
-
-               } else {
-                       $structure = array(
-                               'type'               => $type,
-                               'type_fields'        => 
!empty($headers['content-type']['fields']) ? $headers['content-type']['fields'] 
: array(),
-                               'subtype'            => $subtype,
-                               'content_id'         => 
isset($headers['content-id']) ? $headers['content-id'] : null,
-                               'encoding'           => 
isset($headers['content-transfer-encoding']) ? 
strtolower($headers['content-transfer-encoding']) : '8bit',
-                               'disposition'        => 
isset($headers['content-disposition']) ? 
strtolower($headers['content-disposition']['value']) : null,
-                               'disposition_fields' => 
isset($headers['content-disposition']) ? 
$headers['content-disposition']['fields'] : array(),
-                               'description'        => 
isset($headers['content-description']) ? $headers['content-description'] : null,
-                               'data'               => $data
-                       );
-               }
-
-               return $structure;
-       }
+    /**
+     * Takes a date, removes comments and cleans up some common formatting 
inconsistencies
+     *
+     * @param string $date  The date to clean
+     * @return string  The cleaned date
+     */
+    private static function cleanDate($date)
+    {
+        $date = preg_replace('#\([^)]+\)#', ' ', trim($date));
+        $date = preg_replace('#\s+#', ' ', $date);
+        $date = preg_replace('#(\d+)-([a-z]+)-(\d{4})#i', '\1 \2 \3', $date);
+        $date = preg_replace('#^[a-z]+\s*,\s*#i', '', trim($date));
+        return trim($date);
+    }
+
+    /**
+     * Decodes encoded-word headers of any encoding into raw UTF-8
+     *
+     * @param string $text  The header value to decode
+     * @return string  The decoded UTF-8
+     */
+    private static function decodeHeader($text)
+    {
+        $parts = preg_split('#(=\?[^\?]+\?[QB]\?[^\?]+\?=)#i', $text, -1, 
PREG_SPLIT_DELIM_CAPTURE);
+
+        $part_with_encoding = array();
+        $output = '';
+        foreach ($parts as $part) {
+            if ($part === '') {
+                continue;
+            }
+
+            if (preg_match_all('#=\?([^\?]+)\?([QB])\?([^\?]+)\?=#i', $part, 
$matches, PREG_SET_ORDER)) {
+                foreach ($matches as $match) {
+                    if (strtoupper($match[2]) == 'Q') {
+                        $part_string = rawurldecode(strtr(
+                            $match[3],
+                            array(
+                                '=' => '%',
+                                '_' => ' '
+                            )
+                        ));
+                    } else {
+                        $part_string = base64_decode($match[3]);
+                    }
+                    $lower_encoding = strtolower($match[1]);
+                    $last_key = count($part_with_encoding) - 1;
+                    if (isset($part_with_encoding[$last_key]) && 
$part_with_encoding[$last_key]['encoding'] == $lower_encoding) {
+                        $part_with_encoding[$last_key]['string'] .= 
$part_string;
+                    } else {
+                        $part_with_encoding[] = array('encoding' => 
$lower_encoding, 'string' => $part_string);
+                    }
+                }
+            } else {
+                $last_key = count($part_with_encoding) - 1;
+                if (isset($part_with_encoding[$last_key]) && 
$part_with_encoding[$last_key]['encoding'] == 'iso-8859-1') {
+                    $part_with_encoding[$last_key]['string'] .= $part;
+                } else {
+                    $part_with_encoding[] = array('encoding' => 'iso-8859-1', 
'string' => $part);
+                }
+            }
+        }
+
+        foreach ($part_with_encoding as $part) {
+            $output .= self::iconv($part['encoding'], 'UTF-8', 
$part['string']);
+        }
+
+        return $output;
+    }
+
+    /**
+     * Handles an individual part of a multipart message
+     *
+     * @param array  $info       An array of information about the message
+     * @param array  $structure  An array describing the structure of the 
message
+     * @return array  The modified $info array
+     */
+    private static function handlePart($info, $structure)
+    {
+        if ($structure['type'] == 'multipart') {
+            foreach ($structure['parts'] as $part) {
+                $info = self::handlePart($info, $part);
+            }
+            return $info;
+        }
+
+        if ($structure['type'] == 'application' && 
in_array($structure['subtype'], array('pkcs7-mime', 'x-pkcs7-mime'))) {
+            $to = null;
+            if (isset($info['headers']['to'][0])) {
+                $to = $info['headers']['to'][0]['mailbox'];
+                if (!empty($info['headers']['to'][0]['host'])) {
+                    $to .= '@' . $info['headers']['to'][0]['host'];
+                }
+            }
+        }
+
+        if ($structure['type'] == 'application' && 
in_array($structure['subtype'], array('pkcs7-signature', 'x-pkcs7-signature'))) 
{
+            $from = null;
+            if (isset($info['headers']['from'])) {
+                $from = $info['headers']['from']['mailbox'];
+                if (!empty($info['headers']['from']['host'])) {
+                    $from .= '@' . $info['headers']['from']['host'];
+                }
+            }
+        }
+
+        $data = $structure['data'];
+
+        if ($structure['encoding'] == 'base64') {
+            $content = '';
+            foreach (explode("\r\n", $data) as $line) {
+                $content .= base64_decode($line);
+            }
+        } elseif ($structure['encoding'] == 'quoted-printable') {
+            $content = quoted_printable_decode($data);
+        } else {
+            $content = $data;
+        }
+
+        if ($structure['type'] == 'text') {
+            $charset = 'iso-8859-1';
+            foreach ($structure['type_fields'] as $field => $value) {
+                if (strtolower($field) == 'charset') {
+                    $charset = $value;
+                    break;
+                }
+            }
+            $content = self::iconv($charset, 'UTF-8', $content);
+            if ($structure['subtype'] == 'html') {
+                $content = 
preg_replace('#(content=(["\'])text/html\s*;\s*charset=(["\']?))' . 
preg_quote($charset, '#') . '(\3\2)#i', '\1utf-8\4', $content);
+            }
+        }
+
+        // This indicates a content-id which is used for multipart/related
+        if ($structure['content_id']) {
+            if (!isset($info['related'])) {
+                $info['related'] = array();
+            }
+            $cid = $structure['content_id'][0] == '<' ? 
substr($structure['content_id'], 1, -1) : $structure['content_id'];
+            $info['related']['cid:' . $cid] = array(
+                'mimetype' => $structure['type'] . '/' . $structure['subtype'],
+                'data'     => $content
+            );
+            return $info;
+        }
+
+
+        $has_disposition = !empty($structure['disposition']);
+        $is_text         = $structure['type'] == 'text' && 
$structure['subtype'] == 'plain';
+        $is_html         = $structure['type'] == 'text' && 
$structure['subtype'] == 'html';
+
+        // If the part doesn't have a disposition and is not the default text 
or html, set the disposition to inline
+        if (!$has_disposition && ((!$is_text || !empty($info['text'])) && 
(!$is_html || !empty($info['html'])))) {
+            $is_web_image = $structure['type'] == 'image' && 
in_array($structure['subtype'], array('gif', 'png', 'jpeg', 'pjpeg'));
+            $structure['disposition'] = $is_text || $is_html || $is_web_image 
? 'inline' : 'attachment';
+            $structure['disposition_fields'] = array();
+            $has_disposition = true;
+        }
+
+
+        // Attachments or inline content
+        if ($has_disposition) {
+            $filename = '';
+            foreach ($structure['disposition_fields'] as $field => $value) {
+                if (strtolower($field) == 'filename') {
+                    $filename = $value;
+                    break;
+                }
+            }
+            foreach ($structure['type_fields'] as $field => $value) {
+                if (strtolower($field) == 'name') {
+                    $filename = $value;
+                    break;
+                }
+            }
+
+            // This automatically handles primary content that has a 
content-disposition header on it
+            if ($structure['disposition'] == 'inline' && $filename === '') {
+                if ($is_text && !isset($info['text'])) {
+                    $info['text'] = $content;
+                    return $info;
+                }
+                if ($is_html && !isset($info['html'])) {
+                    $info['html'] = $content;
+                    return $info;
+                }
+            }
+
+            if (!isset($info[$structure['disposition']])) {
+                $info[$structure['disposition']] = array();
+            }
+
+            $info[$structure['disposition']][] = array(
+                'filename' => $filename,
+                'mimetype' => $structure['type'] . '/' . $structure['subtype'],
+                'data'     => $content,
+                'description' => $structure['description'],
+            );
+            return $info;
+        }
+
+        if ($is_text) {
+            $info['text'] = $content;
+            return $info;
+        }
+
+        if ($is_html) {
+            $info['html'] = $content;
+            return $info;
+        }
+    }
+
+    /**
+     * This works around a bug in MAMP 1.9.4+ and PHP 5.3 where iconv()
+     * does not seem to properly assign the return value to a variable, but
+     * does work when returning the value.
+     *
+     * @param string $in_charset   The incoming character encoding
+     * @param string $out_charset  The outgoing character encoding
+     * @param string $string       The string to convert
+     * @return string  The converted string
+     */
+    private static function iconv($in_charset, $out_charset, $string)
+    {
+        return iconv($in_charset, $out_charset, $string);
+    }
+
+    /**
+     * Parses a string representation of an email into the persona, mailbox 
and host parts
+     *
+     * @param  string $string  The email string to parse
+     * @return array  An associative array with the key `mailbox`, and 
possibly `host` and `personal`
+     */
+    private static function parseEmail($string)
+    {
+        $email_regex = 
'((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")(?:\.[ 
\t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ 
\t]*))*)@((?:[a-z0-9\\-]+\.)+[a-z]{2,}|\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\])';
+        $name_regex  = '((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ 
\t]*|"[^"\\\\\n\r]+"[ \t]*)(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ 
\t]*|"[^"\\\\\n\r]+"[ \t]*))*)';
+
+        if (preg_match('~^[ \t]*' . $name_regex . '[ \t]*<[ \t]*' . 
$email_regex . '[ \t]*>[ \t]*$~ixD', $string, $match)) {
+            $match[1] = trim($match[1]);
+            if ($match[1][0] == '"' && substr($match[1], -1) == '"') {
+                $match[1] = substr($match[1], 1, -1);
+            }
+            return array(
+                'personal' => self::decodeHeader($match[1]),
+                'mailbox' => self::decodeHeader($match[2]),
+                'host' => self::decodeHeader($match[3]),
+                'raw' => $string,
+            );
+        } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ 
\t]*>)?[ \t]*$~ixD', $string, $match)) {
+            return array(
+                'mailbox' => self::decodeHeader($match[1]),
+                'host' => self::decodeHeader($match[2]),
+                'raw' => $string,
+            );
+
+            // This handles the outdated practice of including the personal
+            // part of the email in a comment after the email address
+        } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ 
\t]*>)?[ \t]*\(([^)]+)\)[ \t]*$~ixD', $string, $match)) {
+            $match[3] = trim($match[1]);
+            if ($match[3][0] == '"' && substr($match[3], -1) == '"') {
+                $match[3] = substr($match[3], 1, -1);
+            }
+
+            return array(
+                'personal' => self::decodeHeader($match[3]),
+                'mailbox' => self::decodeHeader($match[1]),
+                'host' => self::decodeHeader($match[2]),
+                'raw' => $string,
+            );
+        }
+
+        if (strpos($string, '@') !== false) {
+            list ($mailbox, $host) = explode('@', $string, 2);
+            return array(
+                'mailbox' => self::decodeHeader($mailbox),
+                'host' => self::decodeHeader($host),
+                'raw' => $string,
+            );
+        }
+
+        return array(
+            'mailbox' => self::decodeHeader($string),
+            'host' => '',
+            'raw' => $string,
+        );
+    }
+
+    /**
+     * Parses full email headers into an associative array
+     *
+     * @param  string $headers  The header to parse
+     * @param  string $filter   Remove any headers that match this
+     * @return array  The parsed headers
+     */
+    private static function parseHeaders($headers, $filter = null)
+    {
+        $headers = trim($headers);
+        if (!strlen($headers)) {
+            return array();
+        }
+        $header_lines = preg_split("#\r\n(?!\s)#", $headers);
+
+        $single_email_fields    = array('from', 'sender', 'reply-to');
+        $multi_email_fields     = array('to', 'cc');
+        $additional_info_fields = array('content-type', 'content-disposition');
+
+        $parsed_headers = array();
+        foreach ($header_lines as $header_line) {
+            $header_line = preg_replace("#\r\n\s+#", ' ', $header_line);
+            $header_line = trim($header_line);
+
+            list ($header, $value) = preg_split('#:\s*#', $header_line, 2);
+            $header = strtolower($header);
+
+            if ($filter !== null && strpos($header, $filter) !== false) {
+                continue;
+            }
+
+            $is_single_email          = in_array($header, 
$single_email_fields);
+            $is_multi_email           = in_array($header, $multi_email_fields);
+            $is_additional_info_field = in_array($header, 
$additional_info_fields);
+
+            if ($is_additional_info_field) {
+                $pieces = preg_split('#;\s*#', $value, 2);
+                $value = $pieces[0];
+
+                $parsed_headers[$header] = array('value' => 
self::decodeHeader($value));
+
+                $fields = array();
+                if (!empty($pieces[1])) {
+                    preg_match_all('#(\w+)=("([^"]+)"|([^\s;]+))(?=;|$)#', 
$pieces[1], $matches, PREG_SET_ORDER);
+                    foreach ($matches as $match) {
+                        $fields[strtolower($match[1])] = 
self::decodeHeader(!empty($match[4]) ? $match[4] : $match[3]);
+                    }
+                }
+                $parsed_headers[$header]['fields'] = $fields;
+            } elseif ($is_single_email) {
+                $parsed_headers[$header] = self::parseEmail($value);
+            } elseif ($is_multi_email) {
+                $strings = array();
+
+                preg_match_all('#"[^"]+?"#', $value, $matches, PREG_SET_ORDER);
+                foreach ($matches as $i => $match) {
+                    $strings[] = $match[0];
+                    $value = preg_replace('#' . preg_quote($match[0], '#') . 
'#', ':string' . sizeof($strings), $value, 1);
+                }
+                preg_match_all('#\([^)]+?\)#', $value, $matches, 
PREG_SET_ORDER);
+                foreach ($matches as $i => $match) {
+                    $strings[] = $match[0];
+                    $value = preg_replace('#' . preg_quote($match[0], '#') . 
'#', ':string' . sizeof($strings), $value, 1);
+                }
+
+                $emails = explode(',', $value);
+                array_map('trim', $emails);
+                foreach ($strings as $i => $string) {
+                    $emails = preg_replace(
+                        '#:string' . ($i + 1) . '\b#',
+                        strtr($string, array('\\' => '\\\\', '$' => '\\$')),
+                        $emails,
+                        1
+                    );
+                }
+
+                $parsed_headers[$header] = array();
+                foreach ($emails as $email) {
+                    $parsed_headers[$header][] = self::parseEmail($email);
+                }
+            } elseif ($header == 'references') {
+                $parsed_headers[$header] = 
array_map(array('Flourish\\Mailbox', 'decodeHeader'), 
preg_split('#(?<=>)\s+(?=<)#', $value));
+            } elseif ($header == 'received') {
+                if (!isset($parsed_headers[$header])) {
+                    $parsed_headers[$header] = array();
+                }
+                $parsed_headers[$header][] = preg_replace('#\s+#', ' ', 
self::decodeHeader($value));
+            } else {
+                $parsed_headers[$header] = self::decodeHeader($value);
+            }
+        }
+
+        return $parsed_headers;
+    }
+
+    /**
+     * Parses a MIME message into an associative array of information
+     *
+     * The output includes the following keys:
+     *
+     *  - `'received'`: The date the message was received by the server
+     *  - `'headers'`: An associative array of mail headers, the keys are the 
header names, in lowercase
+     *
+     * And one or more of the following:
+     *
+     *  - `'text'`: The plaintext body
+     *  - `'html'`: The HTML body
+     *  - `'attachment'`: An array of attachments, each containing:
+     *   - `'filename'`: The name of the file
+     *   - `'mimetype'`: The mimetype of the file
+     *   - `'data'`: The raw contents of the file
+     *  - `'inline'`: An array of inline files, each containing:
+     *   - `'filename'`: The name of the file
+     *   - `'mimetype'`: The mimetype of the file
+     *   - `'data'`: The raw contents of the file
+     *  - `'related'`: An associative array of related files, such as embedded 
images, with the key `'cid:{content-id}'` and an array value containing:
+     *   - `'mimetype'`: The mimetype of the file
+     *   - `'data'`: The raw contents of the file
+     *  - `'verified'`: If the message contents were verified via an S/MIME 
certificate - if not verified the smime.p7s will be listed as an attachment
+     *  - `'decrypted'`: If the message contents were decrypted via an S/MIME 
private key - if not decrypted the smime.p7m will be listed as an attachment
+     *
+     * All values in `headers`, `text` and `body` will have been decoded to
+     * UTF-8. Files in the `attachment`, `inline` and `related` array will all
+     * retain their original encodings.
+     *
+     * @param string  $message           The full source of the email message
+     * @param boolean $convert_newlines  If `\r\n` should be converted to `\n` 
in the `text` and `html` parts the message
+     * @return array  The parsed email message - see method description for 
details
+     */
+    public static function parseMessage($message, $convert_newlines = false)
+    {
+        $info = array();
+        list ($headers, $body)   = explode("\r\n\r\n", $message, 2);
+        $parsed_headers          = self::parseHeaders($headers);
+        $info['received']        = 
self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', 
$parsed_headers['received'][0]));
+        $info['headers']         = array();
+        foreach ($parsed_headers as $header => $value) {
+            if (substr($header, 0, 8) == 'content-') {
+                continue;
+            }
+            $info['headers'][$header] = $value;
+        }
+        $info['raw_headers'] = $headers;
+        $info['raw_message'] = $message;
+
+        $info = self::handlePart($info, self::parseStructure($body, 
$parsed_headers));
+        unset($info['raw_message']);
+        unset($info['raw_headers']);
+
+        if ($convert_newlines) {
+            if (isset($info['text'])) {
+                $info['text'] = str_replace("\r\n", "\n", $info['text']);
+            }
+            if (isset($info['html'])) {
+                $info['html'] = str_replace("\r\n", "\n", $info['html']);
+            }
+        }
+
+        if (isset($info['text'])) {
+            $info['text'] = preg_replace('#\r?\n$#D', '', $info['text']);
+        }
+        if (isset($info['html'])) {
+            $info['html'] = preg_replace('#\r?\n$#D', '', $info['html']);
+        }
+
+        return $info;
+    }
+
+    /**
+     * Takes the raw contents of a MIME message and creates an array that
+     * describes the structure of the message
+     *
+     * @param string $data     The contents to get the structure of
+     * @param string $headers  The parsed headers for the message - if not 
present they will be extracted from the `$data`
+     * @return array  The multi-dimensional, associative array containing the 
message structure
+     */
+    private static function parseStructure($data, $headers = null)
+    {
+        if (!$headers) {
+            list ($headers, $data) = preg_split("#^\r\n|\r\n\r\n#", $data, 2);
+            $headers = self::parseHeaders($headers);
+        }
+
+        if (!isset($headers['content-type'])) {
+            $headers['content-type'] = array(
+                'value'  => 'text/plain',
+                'fields' => array()
+            );
+        }
+
+        list ($type, $subtype) = explode('/', 
strtolower($headers['content-type']['value']), 2);
+
+        if ($type == 'multipart') {
+            $structure    = array(
+                'type'    => $type,
+                'subtype' => $subtype,
+                'parts'   => array()
+            );
+            $boundary     = $headers['content-type']['fields']['boundary'];
+            $start_pos    = strpos($data, '--' . $boundary) + 
strlen($boundary) + 4;
+            $end_pos      = strrpos($data, '--' . $boundary . '--') - 2;
+            $sub_contents = explode("\r\n--" . $boundary . "\r\n", substr(
+                $data,
+                $start_pos,
+                $end_pos - $start_pos
+            ));
+            foreach ($sub_contents as $sub_content) {
+                $structure['parts'][] = self::parseStructure($sub_content);
+            }
+        } else {
+            $structure = array(
+                'type'               => $type,
+                'type_fields'        => 
!empty($headers['content-type']['fields']) ? $headers['content-type']['fields'] 
: array(),
+                'subtype'            => $subtype,
+                'content_id'         => isset($headers['content-id']) ? 
$headers['content-id'] : null,
+                'encoding'           => 
isset($headers['content-transfer-encoding']) ? 
strtolower($headers['content-transfer-encoding']) : '8bit',
+                'disposition'        => isset($headers['content-disposition']) 
? strtolower($headers['content-disposition']['value']) : null,
+                'disposition_fields' => isset($headers['content-disposition']) 
? $headers['content-disposition']['fields'] : array(),
+                'description'        => isset($headers['content-description']) 
? $headers['content-description'] : null,
+                'data'               => $data
+            );
+        }
+
+        return $structure;
+    }
 }
diff --git a/lib/group-navbar.php b/lib/group-navbar.php
new file mode 100644
index 0000000..e8d92bc
--- /dev/null
+++ b/lib/group-navbar.php
@@ -0,0 +1,30 @@
+<?php
+
+function navbar($g, $f, $l, $i)
+{
+    echo '  <table class="standard">' . "\n";
+    echo '   <tr>' . "\n";
+    echo '    <th class="nav">';
+    if ($i > $f) {
+        $p = max($i - 20, $f);
+        echo "<a href=\"/" . htmlspecialchars($g, ENT_QUOTES, "UTF-8") . 
"/start/$p\">",
+            "<b>&laquo; <span>previous</span></b></a>";
+    } else {
+        echo "&nbsp;";
+    }
+    echo '</th>' . "\n";
+    $j = min($i + 20, $l);
+    $c = $l - $f + 1;
+    echo '    <th class="align-center">' . htmlspecialchars($g, ENT_QUOTES, 
"UTF-8") . " ($i-$j of $c)</th>\n";
+    echo '    <th class="nav align-right">';
+    if ($i + 20 <= $l) {
+        $n = min($i + 20, $l - 19);
+        echo "<a href=\"/", htmlspecialchars($g, ENT_QUOTES, "UTF-8") . 
"/start/$n\">",
+            "<b><span>next</span> &raquo;</b></a>";
+    } else {
+        echo "&nbsp;";
+    }
+    echo '</th>' . "\n";
+    echo '   </tr>' . "\n";
+    echo '  </table>' . "\n";
+}

Reply via email to