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> ";
+ }
+ 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> ";
+ }
+ 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):.+?)(>|\\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) == ">") {
- 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):.+?)(>|\\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) == ">") {
+ 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&article=$article&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&article=$article&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> ";
- }
- 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> ";
- }
- 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>« <span>previous</span></b></a>';
- } else {
- echo ' ';
- }
-
- 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> »</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>«
<span>previous</span></b></a>';
+} else {
+ echo ' ';
}
-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> »</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(" ", " ",
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(" ", " ",
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(" ", " ", 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(" ", " ", $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>« <span>previous</span></b></a>";
- } else {
- echo " ";
- }
- 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> »</b></a>";
- }
- else {
- echo " ";
- }
- 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&format=rss\">rss</a>";
- }
- echo "</td>\n";
- echo " <td>";
- if ($details['status'] != 'n') {
- echo "<a
href=\"group.php?group=$group&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&format=rss\">rss</a>";
+ }
+ echo "</td>\n";
+ echo " <td>";
+ if ($details['status'] != 'n') {
+ echo "<a href=\"group.php?group=$group&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(" ", " ", 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(" ", " ", 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(" ", " ", 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(" ", " ", $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>« <span>previous</span></b></a>";
+ } else {
+ echo " ";
+ }
+ 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> »</b></a>";
+ } else {
+ echo " ";
+ }
+ echo '</th>' . "\n";
+ echo ' </tr>' . "\n";
+ echo ' </table>' . "\n";
+}