*
* This file contains functions needed to handle headers in mime messages.
*
- * @copyright 2003-2013 The SquirrelMail Project Team
+ * @copyright 2003-2024 The SquirrelMail Project Team
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @version $Id$
* @package squirrelmail
* @since 1.3.0
*/
class Rfc822Header {
+ /**
+ * All headers, unparsed
+ * @var array
+ */
+ var $raw_headers = array();
/**
* Date header
* @var mixed
*/
var $dnt = '';
/**
- * Delivery notification (DR)
+ * Address for requesting message delivery status notification (DSN)
* @var mixed
*/
- var $drnt = '';
+ var $dsn = '';
/**
* @var mixed
*/
foreach ($hdr as $line) {
$pos = strpos($line, ':');
if ($pos > 0) {
+ $this->raw_headers[] = $line;
$field = substr($line, 0, $pos);
if (!strstr($field,' ')) { /* valid field */
$value = trim(substr($line, $pos+1));
$result = '';
$cnt = strlen($value);
for ($i = 0; $i < $cnt; ++$i) {
- switch ($value{$i}) {
+ switch ($value[$i]) {
case '"':
$result .= '"';
- while ((++$i < $cnt) && ($value{$i} != '"')) {
- if ($value{$i} == '\\') {
+ while ((++$i < $cnt) && ($value[$i] != '"')) {
+ if ($value[$i] == '\\') {
$result .= '\\';
++$i;
}
- $result .= $value{$i};
+ $result .= $value[$i];
}
if($i < $cnt) {
- $result .= $value{$i};
+ $result .= $value[$i];
}
break;
case '(':
$depth = 1;
while (($depth > 0) && (++$i < $cnt)) {
- switch($value{$i}) {
+ switch($value[$i]) {
case '\\':
++$i;
break;
}
break;
default:
- $result .= $value{$i};
+ $result .= $value[$i];
break;
}
}
break;
case 'return-receipt-to':
$value = $this->stripComments($value);
- $this->drnt = $this->parseAddress($value);
+ $this->dsn = $this->parseAddress($value);
break;
case 'mime-version':
$value = $this->stripComments($value);
$this->mlist('id', $value);
break;
case 'x-spam-status':
+ case 'x-spam-score':
$this->x_spam_status = $this->parseSpamStatus($value);
break;
+ case 'x-sm-flag-reply':
+ $this->x_sm_flag_reply = $value;
+ break;
default:
break;
}
$iCnt = strlen($address);
$i = 0;
while ($i < $iCnt) {
- $cChar = $address{$i};
+ $cChar = $address[$i];
switch($cChar)
{
case '<':
$iEnd = strpos($address,$cChar,$i+1);
if ($iEnd) {
// skip escaped quotes
- $prev_char = $address{$iEnd-1};
+ $prev_char = $address[$iEnd-1];
while ($prev_char === '\\' && substr($address,$iEnd-2,2) !== '\\\\') {
$iEnd = strpos($address,$cChar,$iEnd+1);
if ($iEnd) {
- $prev_char = $address{$iEnd-1};
+ $prev_char = $address[$iEnd-1];
} else {
$prev_char = false;
}
$iDepth = 1;
$iComment = $i;
while (($iDepth > 0) && (++$iComment < $iCnt)) {
- $cCharComment = $address{$iComment};
+ $cCharComment = $address[$iComment];
switch($cCharComment) {
case '\\':
++$iComment;
// check the next token in case comments appear in the middle of email addresses
$prevToken = end($aTokens);
if (!in_array($prevToken,$aSpecials,true)) {
- if ($i+1<strlen($address) && !in_array($address{$i+1},$aSpecials,true)) {
+ if ($i+1<strlen($address) && !in_array($address[$i+1],$aSpecials,true)) {
$iEnd = strpos($address,' ',$i+1);
if ($iEnd) {
$sNextToken = trim(substr($address,$i+1,$iEnd - $i -1));
$sPersonal = $sEmail = $sGroup = '';
$aStack = $aComment = array();
foreach ($aTokens as $sToken) {
- $cChar = $sToken{0};
+ $cChar = $sToken[0];
switch ($cChar)
{
case '=':
if ($pos > 0) {
$key = trim(substr($prop, 0, $pos));
$val = trim(substr($prop, $pos+1));
- if (strlen($val) > 0 && $val{0} == '"') {
+ if (strlen($val) > 0 && $val[0] == '"') {
$val = substr($val, 1, -1);
}
$propResultArray[$key] = $val;
$value_a = explode(',', $value);
foreach ($value_a as $val) {
$val = trim($val);
- if ($val{0} == '<') {
+ if ($val[0] == '<') {
$val = substr($val, 1, -1);
}
if (substr($val, 0, 7) == 'mailto:') {
}
/**
- * Parses the X-Spam-Status header
+ * Parses the X-Spam-Status or X-Spam-Score header
* @param string $value
*/
function parseSpamStatus($value) {
// Header value looks like this:
// No, score=1.5 required=5.0 tests=MSGID_FROM_MTA_ID,NO_REAL_NAME,UPPERCASE_25_50 autolearn=disabled version=3.1.0-gr0
+ // Update circa 2018, this header can also be simply:
+ // No, score=1.5
+ // So we make the rest of the line optional (there are likely other permutations, so
+ // each element is made optional except the first two... maybe even that's not flexible enough)
+ //
+ // Also now allow parsing of X-Spam-Score header, whose value is just a float
$spam_status = array();
- if (preg_match ('/^(No|Yes),\s+score=(-?\d+\.\d+)\s+required=(-?\d+\.\d+)\s+tests=(.*?)\s+autolearn=(.*?)\s+version=(.+?)$/', $value, $matches)) {
+ if (preg_match ('/^(?:(No|Yes),\s+score=)?(-?\d+\.\d+)(?:\s+required=(-?\d+\.\d+))?(?:\s+tests=(.*?))?(?:\s+autolearn=(.*?))?(?:\s+version=(.+?))?$/i', $value, $matches)) {
+
// full header
$spam_status['bad_format'] = 0;
$spam_status['value'] = $matches[0];
+
// is_spam
- if (isset($matches[1])
- && strtolower($matches[1]) == 'yes') {
- $spam_status['is_spam'] = true;
- } else {
- $spam_status['is_spam'] = false;
+ if (!empty($matches[1])) {
+ if (strtolower($matches[1]) == 'yes')
+ $spam_status['is_spam'] = true;
+ else
+ $spam_status['is_spam'] = false;
}
// score
- $spam_status['score'] = $matches[2];
+ if (!empty($matches[2]))
+ $spam_status['score'] = $matches[2];
// required
- $spam_status['required'] = $matches[3];
+ if (!empty($matches[3]))
+ $spam_status['required'] = $matches[3];
// tests
- $tests = array();
- $tests = explode(',', $matches[4]);
- foreach ($tests as $test) {
- $spam_status['tests'][] = trim($test);
+ if (isset($matches[4])) {
+ $tests = array();
+ $tests = explode(',', $matches[4]);
+ foreach ($tests as $test) {
+ $spam_status['tests'][] = trim($test);
+ }
}
// autolearn
- $spam_status['autolearn'] = $matches[5];
+ if (isset($matches[5]))
+ $spam_status['autolearn'] = $matches[5];
// version
- $spam_status['version'] = $matches[6];
+ if (isset($matches[6]))
+ $spam_status['version'] = $matches[6];
+
} else {
$spam_status['bad_format'] = 1;
$spam_status['value'] = $value;
* Looks through this list of addresses and
* returns the array index (an integer even
* if the array is given with keys of a
- * different type) of the *last* matching
+ * different type) of the first matching
* $address found in this message's
* TO or CC headers, unless there is an exact
* match (meaning that the "personal
$i=0;
foreach($address as $argument) {
$match = $this->findAddress($argument, true);
- if ($match[1]) {
+ if ($match[1]) { // this indicates when the personal information matched
return $i;
} else {
- if (count($match[0]) && !$result) {
+ if (count($match[0]) && $result === FALSE) {
$result = $i;
}
}