Source for file Rfc822Header.class.php
Documentation is available at Rfc822Header.class.php
* This file contains functions needed to handle headers in mime messages.
* @copyright 2003-2020 The SquirrelMail Project Team
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @version $Id: Rfc822Header.class.php 14840 2020-01-07 07:42:38Z pdontthink $
* input: header_string or array
* You must call parseHeader() function after creating object in order to fill object's
* @todo FIXME: there is no constructor function and class should ignore all input args.
* Original date header as fallback for unparsable dates
* Mail-Followup-To header
* SpamAssassin 'x-spam-status' header
* only needed for constructing headers in delivery class
* @param mixed $hdr string or array with message headers
/* First we replace \r\n by \n and unfold the header */
/* FIXME: unfolding header with multiple spaces "\n( +)" */
$hdr =
trim(str_replace(array("\r\n", "\n\t", "\n "),array("\n", ' ', ' '), $hdr));
/* Now we can make a new header array with */
/* each element representing a headerline */
foreach ($hdr as $line) {
$field =
substr($line, 0, $pos);
if (!strstr($field,' ')) { /* valid field */
for ($i =
0; $i <
$cnt; ++
$i) {
while ((++
$i <
$cnt) &&
($value{$i} !=
'"')) {
if ($value{$i} ==
'\\') {
while (($depth >
0) &&
(++
$i <
$cnt)) {
* Parse header field according to field type
* @param string $field field name
* @param string $value field value
$d =
strtr($value, array(' ' =>
' '));
case 'x-confirm-reading-to':
case 'return-receipt-to':
case 'disposition-notification-to':
$this->mime =
($value ==
'1.0' ?
true :
$this->mime);
case 'content-disposition':
$this->mlist('post', $value);
$this->mlist('reply', $value);
$this->mlist('subscribe', $value);
$this->mlist('unsubscribe', $value);
$this->mlist('archive', $value);
$this->mlist('owner', $value);
$this->mlist('help', $value);
$this->mlist('id', $value);
$this->x_sm_flag_reply =
$value;
$aSpecials =
array('(' ,'<' ,',' ,';' ,':');
$aReplace =
array(' (',' <',' ,',' ;',' :');
$iEnd =
strpos($address,'>',$i+
1);
$sToken =
substr($address,$i);
$sToken =
substr($address,$i,$iEnd -
$i +
1);
if ($sToken) $aTokens[] =
$sToken;
$iEnd =
strpos($address,$cChar,$i+
1);
$prev_char =
$address{$iEnd-
1};
while ($prev_char ===
'\\' &&
substr($address,$iEnd-
2,2) !==
'\\\\') {
$iEnd =
strpos($address,$cChar,$iEnd+
1);
$prev_char =
$address{$iEnd-
1};
$sToken =
substr($address,$i);
// also remove the surrounding quotes
$sToken =
substr($address,$i+
1,$iEnd -
$i -
1);
if ($sToken) $aTokens[] =
$sToken;
$iEnd =
strpos($address,')',$i);
$sToken =
substr($address,$i);
while (($iDepth >
0) &&
(++
$iComment <
$iCnt)) {
$cCharComment =
$address{$iComment};
$sToken =
substr($address,$i,$iComment -
$i +
1);
$sToken =
substr($address,$i,$iEnd -
$i +
1);
// 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)) {
$iEnd =
strpos($address,' ',$i+
1);
$sNextToken =
trim(substr($address,$i+
1,$iEnd -
$i -
1));
// create token and add it again
$sNewToken =
$prevToken .
$sNextToken;
if($sNewToken) $aTokens[] =
$sNewToken;
if ($sToken) $aTokens[] =
$sToken;
$iEnd =
strpos($address,' ',$i+
1);
if ($sToken) $aTokens[] =
$sToken;
* @return object AddressStructure object
//$aStack=explode(' ',implode('',$aStack));
while (count($aStack) &&
!$sEmail) {
if (!$sPersonal &&
count($aComment)) {
if ($sPersonal &&
substr($sPersonal,0,2) ==
'=?') {
$oAddr->personal =
$sPersonal;
// $oAddr->group = $sGroup;
$iPosAt =
strpos($sEmail,'@');
$oAddr->mailbox =
substr($sEmail, 0, $iPosAt);
$oAddr->host =
substr($sEmail, $iPosAt+
1);
$oAddr->mailbox =
$sEmail;
$aStack =
$aComment =
array();
* recursive function for parsing address strings and storing them in an address stucture object.
* personal name: encoded: =?charset?Q|B?string?=
* This function is also used for validating addresses returned from compose
* That's also the reason that the function became a little bit huge
* @param boolean $ar return array instead of only the first element
* @param array $addr_ar (obsolete) array with parsed addresses
* @param string $group (obsolete)
* @param string $host default domainname in case of addresses without a domainname
* @param string $lookup (since) callback function for lookup of address strings which are probably nicks (without @)
* @return mixed array with AddressStructure objects or only one address_structure object.
function parseAddress($address,$ar=
false,$aAddress=
array(),$sGroup=
'',$sHost=
'',$lookup=
false) {
$sPersonal =
$sEmail =
$sComment =
$sGroup =
'';
$aStack =
$aComment =
array();
foreach ($aTokens as $sToken) {
$aComment[] =
substr($sToken,1,-
1);
if(!$oAddr ||
((isset
($oAddr)) &&
!strlen($oAddr->mailbox) &&
!$oAddr->personal)) {
$sEmail =
$sGroup .
':;';
$aStack =
$aComment =
array();
default:
$aStack[] =
$sToken; break;
/* now do the action again for the last address */
/* try to lookup the addresses in case of invalid email addresses */
$aProcessedAddress =
array();
foreach ($aAddress as $oAddr) {
$aAddrBookAddress =
array();
if (isset
($aAddr['email'])) {
if (strpos($aAddr['email'],',')) {
$aAddrBookAddress =
$this->parseAddress($aAddr['email'],true);
$iPosAt =
strpos($aAddr['email'], '@');
$oAddr->mailbox =
$aAddr['email'];
$oAddr->mailbox =
substr($aAddr['email'], 0, $iPosAt);
$oAddr->host =
substr($aAddr['email'], $iPosAt+
1);
if (isset
($aAddr['name'])) {
$oAddr->personal =
$aAddr['name'];
if (!$grouplookup &&
!strlen($oAddr->mailbox)) {
$oAddr->mailbox =
trim($sEmail);
if ($sHost &&
strlen($oAddr->mailbox)) {
} else if (!$grouplookup &&
!$oAddr->host) {
if ($sHost &&
strlen($oAddr->mailbox)) {
if (!$aAddrBookAddress &&
strlen($oAddr->mailbox)) {
$aProcessedAddress[] =
$oAddr;
$aProcessedAddress =
array_merge($aProcessedAddress,$aAddrBookAddress);
return $aProcessedAddress;
if (isset
($aProcessedAddress[0]))
return $aProcessedAddress[0];
* Normalise the different Priority headers into a uniform value,
* namely that of the X-Priority header (1, 3, 5). Supports:
* Priority, X-Priority, Importance.
* X-MS-Mail-Priority is not parsed because it always coincides
* with one of the other headers.
* NOTE: this is actually a duplicate from the function in
* functions/imap_messages. I'm not sure if it's ok here to call
* @param string $sValue literal priority name
// don't use function call inside array_shift.
if ( $value ==
'urgent' ||
$value ==
'high' ) {
} elseif ( $value ==
'non-urgent' ||
$value ==
'low' ) {
// default is normal priority
* @param string $value content type header
if (!isset
($properties['charset'])) {
$properties['charset'] =
'us-ascii';
* @param array $aParameters
// handle multiline parameters
foreach($aParameters as $key =>
$value) {
if ($iPos =
strpos($key,'*')) {
if (!isset
($aResults[$sKey])) {
$aResults[$sKey] =
$value;
if (substr($key,-
1) ==
'*') { // parameter contains language/charset info
$aResults[$sKey] .=
$value;
$aResults[$key] =
$value;
foreach ($aCharset as $key) {
$value =
$aResults[$key];
// extract the charset & language
/* FIXME: What's the status of charset decode with language information ????
* Maybe language information contains only ascii text and charset_decode()
* only runs sm_encode_html_special_chars() on it. If it contains 8bit information, you
* get html encoded text in charset used by selected translation.
$aResults[$key] =
$value;
$propResultArray =
array();
foreach ($propArray as $prop) {
if (strlen($val) >
0 &&
$val{0} ==
'"') {
$propResultArray[$key] =
$val;
* Fills disposition object in rfc822Header object
$disp->properties =
$props_a;
* Fills mlist array keys in rfc822Header object
function mlist($field, $value) {
foreach ($value_a as $val) {
if (substr($val, 0, 7) ==
'mailto:') {
$res_a['mailto'] =
substr($val, 7);
$this->mlist[$field] =
$res_a;
* Parses the X-Spam-Status or X-Spam-Score header
// 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:
// 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
if (preg_match ('/^(?:(No|Yes),\s+score=)?(-?\d+\.\d+)(?:\s+required=(-?\d+\.\d+))?(?:\s+tests=(.*?))?(?:\s+autolearn=(.*?))?(?:\s+version=(.+?))?$/i', $value, $matches)) {
$spam_status['bad_format'] =
0;
$spam_status['value'] =
$matches[0];
if (!empty($matches[1])) {
$spam_status['is_spam'] =
true;
$spam_status['is_spam'] =
false;
$spam_status['score'] =
$matches[2];
$spam_status['required'] =
$matches[3];
if (isset
($matches[4])) {
$tests =
explode(',', $matches[4]);
foreach ($tests as $test) {
$spam_status['tests'][] =
trim($test);
$spam_status['autolearn'] =
$matches[5];
$spam_status['version'] =
$matches[6];
$spam_status['bad_format'] =
1;
$spam_status['value'] =
$value;
* function to get the address strings out of the header.
* example1: header->getAddr_s('to').
* example2: header->getAddr_s(array('to', 'cc', 'bcc'))
* @param mixed $arr string or array of strings
* @param string $separator
* @param boolean $encoded (since 1.4.0) return encoded or plain text addresses
* @param boolean $unconditionally_quote (since 1.4.21/1.5.2) When TRUE, always
* quote the personal part,
* encoded, otherwise quoting
* personal part is not encoded
function getAddr_s($arr, $separator =
',',$encoded=
false,$unconditionally_quote=
FALSE) {
if ($this->getAddr_s($arg, $separator, $encoded, $unconditionally_quote)) {
$s =
($s ?
substr($s, 2) :
$s);
foreach ($addr as $addr_o) {
$s .=
$addr_o->getEncodedAddress($unconditionally_quote) .
$separator;
$s .=
$addr_o->getAddress(TRUE, FALSE, $unconditionally_quote) .
$separator;
$s .=
$addr->getEncodedAddress($unconditionally_quote);
$s .=
$addr->getAddress(TRUE, FALSE, $unconditionally_quote);
* function to get the array of addresses out of the header.
* @param mixed $arg string or array of strings
* @param array $excl_arr array of excluded email addresses
* @param array $arr array of added email addresses
function getAddr_a($arg, $excl_arr =
array(), $arr =
array()) {
foreach($arg as $argument) {
$arr =
$this->getAddr_a($argument, $excl_arr, $arr);
foreach ($addr as $next_addr) {
if (isset
($next_addr->host) &&
($next_addr->host !=
'')) {
$email =
$next_addr->mailbox .
'@' .
$next_addr->host;
$email =
$next_addr->mailbox;
if ($email &&
!isset
($arr[$email]) &&
!isset
($excl_arr[$email])) {
$arr[$email] =
$next_addr->personal;
$email .=
(isset
($addr->host) ?
'@' .
$addr->host :
'');
if ($email &&
!isset
($arr[$email]) &&
!isset
($excl_arr[$email])) {
$arr[$email] =
$addr->personal;
//FIXME: This needs some documentation (inside the function too)! Don't code w/out comments!
* Looking at the code years after it was written,
* this is my (Paul) best guess as to what this
* function does (note that docs previously claimed
* that this function returns boolean or an array,
* but it no longer appears to return an array - an
* Inspects the TO and CC (or FROM) headers of the
* message represented by this object, looking for
* the address(es) given by $address
* If $address is a string:
* Serves as a test (returns boolean) as to
* whether or not the given address is found
* anywhere in the TO or CC (or FROM) headers
* If $address is an array:
* 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 first matching
* $address found in this message's
* TO or CC (or FROM) headers, unless there
* is an exact match (meaning that the "personal
* information" in addition to the email
* address also matches), in which case that
* index (the first one found) is returned
* @param mixed $address Address(es) to search for in this
* message's TO and CC (or FROM)
* headers - please see above how the
* format of this argument affects the
* return value of this function
* @param boolean $use_from When TRUE, this function will ONLY
* search the FROM headers and NOT the
* TO and CC headers, when FALSE, ONLY
* the TO and CC headers are searched
* (OPTIONAL; default is FALSE)
* @param boolean $recurs FOR INTERNAL USE ONLY
* @return mixed Boolean when $address is a scalar,
* indicating whether or not the address
* was found in the TO or CC headers.
* An integer when $address is an array,
* containing the index of the value in
* that array that was found in the TO
* or CC headers, or boolean FALSE if
* there were no matches at all
function findAddress($address, $use_from=
FALSE, $recurs=
FALSE) {
foreach($address as $argument) {
$match =
$this->findAddress($argument, $use_from, true);
if ($match[1]) { // this indicates when the personal information matched
if (count($match[0]) &&
$result ===
FALSE) {
foreach ($this->from as $from) {
return array($results, true);
foreach ($this->to as $to) {
return array($results, true);
foreach ($this->cc as $cc) {
return array($results, true);
return array($results, false);
} elseif (count($results)) {
* @param string $type0 media type
* @param string $type1 media subtype
* @return array media properties
* @todo check use of media type arguments
Documentation generated on Mon, 13 Jan 2020 04:25:14 +0100 by phpDocumentor 1.4.3